04. 事务 ACID 与隔离级别

数据库事务的 ACID 四大特性实现机制,四种隔离级别与脏读、不可重复读、幻读三种并发异常现象的完整解析。

1. ACID 属性

1.1 四大特性

属性含义InnoDB 实现机制
Atomicity(原子性)事务要么全完成,要么全不完成Undo Log(回滚日志)
Consistency(一致性)事务前后数据库处于一致状态约束、触发器、外键、业务规则
Isolation(隔离性)并发事务互不干扰MVCC + 锁
Durability(持久性)事务提交后数据永久保存Redo Log + WAL(预写日志)

1.2 InnoDB 的事务日志

Undo Log(回滚日志):
  - 记录数据修改前的旧值
  - 事务回滚时,用 Undo Log 恢复数据
  - 作用:原子性 + MVCC 版本链

Redo Log(重做日志):
  - 记录数据修改后的新值
  - 事务提交时,先写 Redo Log,再刷脏页
  - 作用:持久性(崩溃恢复)

Write-Ahead Logging (WAL):
  - 先写日志,再写磁盘
  - 日志顺序写(快) vs 数据页随机写(慢)
  - Redo Log 顺序写提高性能,同时保证不丢数据

2. 四种隔离级别

SQL 标准定义了四种隔离级别,从低到高限制越来越严格,但并发性能逐渐降低。

隔离级别脏读不可重复读幻读InnoDB 实现
READ UNCOMMITTED✅ 允许✅ 允许✅ 允许直接读取最新版本
READ COMMITTED❌ 禁止✅ 允许✅ 允许MVCC(每次读生成 ReadView)
REPEATABLE READ❌ 禁止❌ 禁止⚠️ InnoDB 禁止MVCC + 间隙锁
SERIALIZABLE❌ 禁止❌ 禁止❌ 禁止所有 SELECT 加锁

2.1 三种并发异常

脏读(Dirty Read)

事务 A                    事务 B
  │                        │
  ├──→ 修改余额 = 800
  │     (未提交)          │
  │                        ├──→ 读取余额 = 800  ← 脏读!
  │                        │
  ├──→ ROLLBACK(回滚)
  │     余额恢复 1000       │
  │                        │
  │                        ├──→ 之前读到的 800 是错误数据

不可重复读(Non-repeatable Read)

事务 A                    事务 B
  │                        │
  ├──→ 读取余额 = 1000     │
  │                        ├──→ 修改余额 = 800
  │                        │     COMMIT
  │                        │
  ├──→ 再次读取余额 = 800  ← 同一事务内两次读取结果不同!

幻读(Phantom Read)

事务 A                    事务 B
  │                        │
  ├──→ SELECT COUNT(*) FROM orders  
  │     结果: 10 条          │
  │                        ├──→ INSERT INTO orders ...
  │                        │     COMMIT
  │                        │
  ├──→ SELECT COUNT(*) FROM orders
  │     结果: 11 条  ← 同一事务内两次范围查询行数不同!

2.2 InnoDB 的 RR 如何解决幻读

InnoDB REPEATABLE READ 下:
  - 快照读(普通 SELECT):通过 MVCC + ReadView(事务首次 SELECT 时生成)
    → 整个事务看到一致快照,不会出现幻影行
    
  - 当前读(SELECT ... FOR UPDATE):通过 Next-Key Lock(记录锁 + 间隙锁)
    → 锁定查询范围及间隙,阻止其他事务插入新行

注意:SQL 标准中 RR 允许幻读,但 InnoDB 通过上述机制实际上在大部分场景下解决了幻读。

3. 隔离级别设置

-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置会话级隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 启动事务
START TRANSACTION;
-- 或
BEGIN;

-- 提交 / 回滚
COMMIT;
ROLLBACK;

4. 隔离级别选择建议

场景推荐隔离级别原因
普通 OLTP 应用READ COMMITTED避免锁竞争,Oralce/PostgreSQL 默认
金融交易、对账REPEATABLE READMySQL 默认,防不可重复读
报表统计(快照一致性)REPEATABLE READ事务内数据一致
极端严格同步SERIALIZABLE性能代价高,一般不用

注意:MySQL 默认 RR,Oracle/PostgreSQL/SQL Server 默认 RC。MySQL 的 RR 配合 MVCC 在很多场景下并不比 RC 锁更多。


5. 分布式事务

单体数据库事务可通过 ACID 保证一致性,跨服务调用时需要分布式事务方案:

5.1 两阶段提交(2PC)

阶段一(投票):
  协调者 → 询问所有参与者是否可以提交
  参与者 → 执行本地事务,锁定资源,返回 Yes/No

阶段二(提交/回滚):
  若全部 Yes → 协调者发送 Commit,参与者提交并释放锁
  若任一 No  → 协调者发送 Rollback,参与者回滚并释放锁
特性说明
优点强一致性,理论成熟
缺点同步阻塞、单点故障(协调者)、资源锁定时间长
代表XA 协议(JTA)、Seata AT 模式

5.2 TCC(Try-Confirm-Cancel)

@Compensable
public interface OrderService {
    // Try:预留资源(如冻结库存)
    @Try
    boolean tryCreateOrder(OrderRequest req);
    
    // Confirm:真正执行业务(扣减库存)
    @Confirm
    void confirmOrder(String xid);
    
    // Cancel:释放预留资源(解冻库存)
    @Cancel
    void cancelOrder(String xid);
}

TCC 两阶段均不阻塞,性能优于 2PC,但业务侵入性强,每个操作需实现三个接口。

5.3 Saga 模式

长事务拆分为多个本地事务,每个本地事务有对应的补偿操作:

正常流程:
  T1(扣库存) → T2(创建订单) → T3(扣款) → T4(通知发货)

补偿流程(T3 失败时):
  T3 失败 → C2(取消订单) → C1(恢复库存)
  (注意:无需补偿 T4,因为 T3 失败后不会执行到 T4)
方案一致性性能业务侵入适用场景
2PC/XA强一致低低短事务、金融核心
TCC最终一致高高电商订单、库存
Saga最终一致高中长事务、业务流程
本地消息表最终一致高中异步通知、对账
最大努力通知最终一致最高低对账、非关键业务

5.4 Seata 集成示例

# seata 配置
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  client:
    rm:
      async-commit-buffer-limit: 10000
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
@Service
public class BusinessService {
    
    @Autowired private StorageFeignClient storageClient;
    @Autowired private OrderFeignClient orderClient;
    
    @GlobalTransactional(name = "purchase-tx", rollbackFor = Exception.class)
    public void purchase(String userId, String commodityCode, int count) {
        // 扣减库存
        storageClient.deduct(commodityCode, count);
        // 创建订单
        orderClient.create(userId, commodityCode, count);
        // 任一异常,全局回滚
    }
}

6. 事务性能优化

6.1 事务粒度控制

-- ❌ 反模式:大事务包含过多操作
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 20ms
-- ... 10000 行其他操作 ...
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- 20ms
COMMIT;  -- 总耗时 5s,锁持有 5s

-- ✅ 正解:拆小事务,减少锁持有时间
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 其他操作...
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

6.2 乐观锁 vs 悲观锁

-- 悲观锁(高冲突场景)
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- 乐观锁(低冲突场景)
UPDATE accounts 
SET balance = balance - 100, version = version + 1 
WHERE id = 1 AND version = 5;
-- 若返回 affected_rows = 0,说明版本冲突,需重试
特性悲观锁乐观锁
机制读取时加锁版本号/CAS 验证
适用高冲突、重试成本高低冲突、读多写少
性能阻塞等待可能失败重试
死锁可能发生不可能

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询