Skip to content

事务

要点概述

事务是数据库操作的基本单位,确保多个相关操作要么全部成功执行,要么全部不执行,保证数据的一致性和完整性。

什么是事务?

事务(Transaction)是关系型数据库中的基础概念,它将多个相互关联的数据库操作绑定在一起,形成一个不可分割的工作单元。事务具有"原子性"特点,即一个事务中的所有操作要么全部成功执行,要么全部不执行。

IMPORTANT

事务是保证数据库一致性和可靠性的核心机制。在处理涉及多步骤操作的业务逻辑时(如资金转账、库存管理等),正确使用事务至关重要。

事务的四大特性(ACID)

  • 原子性(Atomicity):事务是不可分割的最小工作单位,要么全部成功执行,要么全部失败回滚
  • 一致性(Consistency):事务执行前后,数据库必须从一个一致状态转变为另一个一致状态
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务
  • 持久性(Durability):事务一旦提交,其对数据库的修改就是永久性的

为什么需要事务?

让我们通过一个经典的银行转账场景来理解事务的重要性:

应用场景

假设需要从张三的账户向李四的账户转账1000元,这个过程至少包括两个步骤:

  1. 从张三的账户扣除1000元
  2. 向李四的账户增加1000元

如果没有事务机制,可能出现以下问题:

  • 第一步成功执行,但第二步失败,导致张三损失了钱,李四却没收到
  • 系统崩溃导致操作记录丢失
  • 其他用户在操作过程中看到不一致的数据状态

PostgreSQL中的事务操作

PostgreSQL通过以下命令来控制事务:

命令说明
BEGIN开始一个事务
COMMIT提交事务,使所有更改永久生效
ROLLBACK回滚事务,取消所有更改
SAVEPOINT在事务中创建一个保存点
ROLLBACK TO回滚到指定的保存点
RELEASE删除一个保存点

基本事务示例

INFO

在PostgreSQL中,每个SQL语句实际上都默认在一个单独的事务中执行。但为了将多个操作组合在一个事务中,我们需要显式使用BEGINCOMMIT命令。

示例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;

执行过程分析:

  1. BEGIN语句开始一个新事务
  2. 执行四个UPDATE语句,但这些更改对其他会话不可见
  3. COMMIT语句使所有更改永久生效,并对其他会话可见
  4. 如果在任何步骤发生错误,可以使用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;

执行过程分析:

  1. 开始事务并从张三账户扣款
  2. 创建名为my_savepoint的保存点
  3. 执行错误操作(给李四而不是王五增加金额)
  4. 使用ROLLBACK TO my_savepoint回滚到保存点,撤销给李四增加金额的操作
  5. 执行正确操作,给王五增加金额
  6. 提交事务,永久保存张三扣款和王五加款的操作

WARNING

回滚到保存点后,该保存点仍然存在,可以多次回滚到同一保存点。但如果不再需要某个保存点,可以使用RELEASE savepoint_name命令释放它。

事务的注意事项

CAUTION

虽然事务为数据库操作提供了保障,但使用不当可能导致性能问题或死锁

  1. 事务应尽量简短:长时间运行的事务会锁定资源,影响系统并发性能
  2. 避免在事务中执行非数据库操作:如网络调用、文件IO等
  3. 注意事务隔离级别:不同隔离级别会影响并发事务的行为
  4. 合理使用保存点:保存点可以提高事务的灵活性,但过多的保存点会增加复杂性
  5. 了解自动提交机制: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);

总结

核心要点

  1. 事务是数据库操作的基本单位,具有ACID特性
  2. 在PostgreSQL中,使用BEGIN开始事务,COMMIT提交事务,ROLLBACK回滚事务
  3. 保存点(SAVEPOINT)允许在事务内创建检查点,可以回滚到特定点而不必放弃整个事务
  4. 事务隔离级别决定了并发事务之间的可见性和交互方式
  5. 合理使用事务可以保证数据的一致性和完整性,尤其在涉及多步骤操作的业务场景中

TIP

在设计数据库应用程序时,应当仔细考虑事务边界,确保将相关操作正确地组织在同一事务中。同时,避免事务过大或过长,以免影响系统性能。

通过掌握PostgreSQL的事务机制,你可以构建更加可靠、一致的数据库应用,确保关键业务操作的正确性和完整性。