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 READ | MySQL 默认,防不可重复读 |
| 报表统计(快照一致性) | 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 验证 |
| 适用 | 高冲突、重试成本高 | 低冲突、读多写少 |
| 性能 | 阻塞等待 | 可能失败重试 |
| 死锁 | 可能发生 | 不可能 |
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。