Appearance
事务
要点概述
事务是数据库操作的基本单位,确保多个相关操作要么全部成功执行,要么全部不执行,保证数据的一致性和完整性。
什么是事务?
事务(Transaction)是关系型数据库中的基础概念,它将多个相互关联的数据库操作绑定在一起,形成一个不可分割的工作单元。事务具有"原子性"特点,即一个事务中的所有操作要么全部成功执行,要么全部不执行。
IMPORTANT
事务是保证数据库一致性和可靠性的核心机制。在处理涉及多步骤操作的业务逻辑时(如资金转账、库存管理等),正确使用事务至关重要。
事务的四大特性(ACID)
- 原子性(Atomicity):事务是不可分割的最小工作单位,要么全部成功执行,要么全部失败回滚
- 一致性(Consistency):事务执行前后,数据库必须从一个一致状态转变为另一个一致状态
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务
- 持久性(Durability):事务一旦提交,其对数据库的修改就是永久性的
为什么需要事务?
让我们通过一个经典的银行转账场景来理解事务的重要性:
应用场景
假设需要从张三的账户向李四的账户转账1000元,这个过程至少包括两个步骤:
- 从张三的账户扣除1000元
- 向李四的账户增加1000元
如果没有事务机制,可能出现以下问题:
- 第一步成功执行,但第二步失败,导致张三损失了钱,李四却没收到
- 系统崩溃导致操作记录丢失
- 其他用户在操作过程中看到不一致的数据状态
PostgreSQL中的事务操作
PostgreSQL通过以下命令来控制事务:
命令 | 说明 |
---|---|
BEGIN | 开始一个事务 |
COMMIT | 提交事务,使所有更改永久生效 |
ROLLBACK | 回滚事务,取消所有更改 |
SAVEPOINT | 在事务中创建一个保存点 |
ROLLBACK TO | 回滚到指定的保存点 |
RELEASE | 删除一个保存点 |
基本事务示例
INFO
在PostgreSQL中,每个SQL语句实际上都默认在一个单独的事务中执行。但为了将多个操作组合在一个事务中,我们需要显式使用BEGIN
和COMMIT
命令。
示例1:基本转账事务
sql
-- 开始事务
BEGIN;
-- 从张三账户扣款
UPDATE accounts SET balance = balance - 100.00
WHERE name = '张三';
-- 更新张三所在分行的余额
UPDATE branches SET balance = balance - 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = '张三');
-- 李四账户增加金额
UPDATE accounts SET balance = balance + 100.00
WHERE name = '李四';
-- 更新李四所在分行的余额
UPDATE branches SET balance = balance + 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = '李四');
-- 提交事务
COMMIT;
执行过程分析:
BEGIN
语句开始一个新事务- 执行四个
UPDATE
语句,但这些更改对其他会话不可见 COMMIT
语句使所有更改永久生效,并对其他会话可见- 如果在任何步骤发生错误,可以使用
ROLLBACK
代替COMMIT
来撤销所有更改
NOTE
在事务执行过程中,所有操作对其他数据库会话都是不可见的,只有在事务提交后,这些更改才会同时对外可见。
事务中的保存点(Savepoint)
保存点是事务中的一个标记,允许选择性地回滚部分事务操作,而不必放弃整个事务。
保存点示例
示例2:使用保存点进行错误修正
sql
-- 开始事务
BEGIN;
-- 从张三账户扣款
UPDATE accounts SET balance = balance - 100.00
WHERE name = '张三';
-- 创建保存点
SAVEPOINT my_savepoint;
-- 错误操作:给错误的账户(李四)增加金额
UPDATE accounts SET balance = balance + 100.00
WHERE name = '李四';
-- 发现错误,回滚到保存点
ROLLBACK TO my_savepoint;
-- 正确操作:给正确的账户(王五)增加金额
UPDATE accounts SET balance = balance + 100.00
WHERE name = '王五';
-- 提交事务
COMMIT;
执行过程分析:
- 开始事务并从张三账户扣款
- 创建名为
my_savepoint
的保存点 - 执行错误操作(给李四而不是王五增加金额)
- 使用
ROLLBACK TO my_savepoint
回滚到保存点,撤销给李四增加金额的操作 - 执行正确操作,给王五增加金额
- 提交事务,永久保存张三扣款和王五加款的操作
WARNING
回滚到保存点后,该保存点仍然存在,可以多次回滚到同一保存点。但如果不再需要某个保存点,可以使用RELEASE savepoint_name
命令释放它。
事务的注意事项
CAUTION
虽然事务为数据库操作提供了保障,但使用不当可能导致性能问题或死锁
- 事务应尽量简短:长时间运行的事务会锁定资源,影响系统并发性能
- 避免在事务中执行非数据库操作:如网络调用、文件IO等
- 注意事务隔离级别:不同隔离级别会影响并发事务的行为
- 合理使用保存点:保存点可以提高事务的灵活性,但过多的保存点会增加复杂性
- 了解自动提交机制:PostgreSQL默认每条语句都在一个独立事务中执行
事务隔离级别
PostgreSQL支持SQL标准定义的四种事务隔离级别:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 描述 |
---|---|---|---|---|
读未提交 (READ UNCOMMITTED) | 可能 | 可能 | 可能 | 一个事务可以看到其他未提交事务的更改 |
读已提交 (READ COMMITTED) | 不可能 | 可能 | 可能 | 一个事务只能看到已提交事务的更改(PostgreSQL默认级别) |
可重复读 (REPEATABLE READ) | 不可能 | 不可能 | 可能 | 确保在同一事务中多次读取相同记录返回相同的结果 |
可序列化 (SERIALIZABLE) | 不可能 | 不可能 | 不可能 | 最高级别的隔离,确保事务的执行结果与串行执行相同 |
TIP
在PostgreSQL中设置事务隔离级别的语法:
sql
BEGIN TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE];
实际应用示例
示例3:电商订单处理
在电商系统中,创建订单涉及多个操作:减少商品库存、创建订单记录、记录支付信息等。
sql
BEGIN;
-- 检查库存是否充足
SELECT quantity FROM inventory WHERE product_id = 101;
-- 假设库存充足,减少库存
UPDATE inventory SET quantity = quantity - 2 WHERE product_id = 101;
-- 创建订单
INSERT INTO orders (order_id, customer_id, order_date, total_amount)
VALUES ('ORD-2023-001', 1001, CURRENT_DATE, 199.98);
-- 添加订单项
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES ('ORD-2023-001', 101, 2, 99.99);
-- 记录支付信息
INSERT INTO payments (payment_id, order_id, payment_method, amount, payment_date)
VALUES ('PAY-2023-001', 'ORD-2023-001', 'credit_card', 199.98, CURRENT_TIMESTAMP);
-- 如果所有操作都成功,提交事务
COMMIT;
示例4:银行转账系统实现
下面是一个完整的银行转账系统示例,展示了如何使用事务和错误处理:
sql
-- 创建账户表
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
balance DECIMAL(15, 2) NOT NULL CHECK (balance >= 0),
branch_name VARCHAR(100) NOT NULL
);
-- 创建分行表
CREATE TABLE branches (
name VARCHAR(100) PRIMARY KEY,
balance DECIMAL(15, 2) NOT NULL
);
-- 创建转账记录表
CREATE TABLE transfers (
id SERIAL PRIMARY KEY,
from_account INT REFERENCES accounts(id),
to_account INT REFERENCES accounts(id),
amount DECIMAL(15, 2) NOT NULL,
transfer_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 转账函数
CREATE OR REPLACE FUNCTION transfer_money(
sender_id INT,
receiver_id INT,
transfer_amount DECIMAL(15, 2)
) RETURNS BOOLEAN AS $$
DECLARE
sender_branch VARCHAR(100);
receiver_branch VARCHAR(100);
BEGIN
-- 开始事务
BEGIN;
-- 检查余额是否足够
IF NOT EXISTS (SELECT 1 FROM accounts WHERE id = sender_id AND balance >= transfer_amount) THEN
RAISE EXCEPTION '账户余额不足';
END IF;
-- 获取分行名称
SELECT branch_name INTO sender_branch FROM accounts WHERE id = sender_id;
SELECT branch_name INTO receiver_branch FROM accounts WHERE id = receiver_id;
-- 更新发送方账户
UPDATE accounts SET balance = balance - transfer_amount
WHERE id = sender_id;
-- 更新发送方分行
UPDATE branches SET balance = balance - transfer_amount
WHERE name = sender_branch;
-- 更新接收方账户
UPDATE accounts SET balance = balance + transfer_amount
WHERE id = receiver_id;
-- 更新接收方分行
UPDATE branches SET balance = balance + transfer_amount
WHERE name = receiver_branch;
-- 记录转账
INSERT INTO transfers (from_account, to_account, amount)
VALUES (sender_id, receiver_id, transfer_amount);
-- 提交事务
COMMIT;
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
-- 发生错误,回滚事务
ROLLBACK;
RAISE NOTICE '转账失败: %', SQLERRM;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
函数使用示例:
sql
-- 初始化数据
INSERT INTO branches VALUES ('北京分行', 1000000), ('上海分行', 2000000);
INSERT INTO accounts (name, balance, branch_name) VALUES
('张三', 10000, '北京分行'),
('李四', 20000, '上海分行');
-- 执行转账
SELECT transfer_money(1, 2, 1000);
总结
核心要点
- 事务是数据库操作的基本单位,具有ACID特性
- 在PostgreSQL中,使用BEGIN开始事务,COMMIT提交事务,ROLLBACK回滚事务
- 保存点(SAVEPOINT)允许在事务内创建检查点,可以回滚到特定点而不必放弃整个事务
- 事务隔离级别决定了并发事务之间的可见性和交互方式
- 合理使用事务可以保证数据的一致性和完整性,尤其在涉及多步骤操作的业务场景中
TIP
在设计数据库应用程序时,应当仔细考虑事务边界,确保将相关操作正确地组织在同一事务中。同时,避免事务过大或过长,以免影响系统性能。
通过掌握PostgreSQL的事务机制,你可以构建更加可靠、一致的数据库应用,确保关键业务操作的正确性和完整性。