数据库事务
事务(Transaction)是一组数据库操作的逻辑单元,要么全部成功,要么全部回滚,保证数据的一致性与完整性。
ACID 四大特性
| 特性 | 含义 | 实现方式(InnoDB) |
|---|---|---|
| 原子性(Atomicity) | 事务内所有操作要么全成功,要么全回滚 | undo log |
| 一致性(Consistency) | 事务执行前后数据满足所有约束(业务规则) | 其余三者共同保证 |
| 隔离性(Isolation) | 并发事务互不干扰,中间状态对外不可见 | MVCC + 锁 |
| 持久性(Durability) | 事务提交后数据永久写入,宕机不丢失 | redo log(WAL) |
并发问题
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到另一事务未提交的数据 | T2 读到 T1 修改但未提交的金额 |
| 不可重复读 | 同一事务内两次读同一行,结果不同(另一事务修改并提交) | T1 两次读余额,中间 T2 扣款提交 |
| 幻读 | 同一事务内两次查询,行数不同(另一事务插入/删除并提交) | T1 两次查用户数,中间 T2 新增一行 |
| 丢失更新 | 两个事务读取同一行后各自修改,后提交者覆盖前者 | 并发抢购库存 |
四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | ❌ 可能 | ❌ 可能 | ❌ 可能 | 最低,几乎不用 |
| READ COMMITTED | ✅ 防止 | ❌ 可能 | ❌ 可能 | Oracle/SQL Server 默认 |
| REPEATABLE READ | ✅ 防止 | ✅ 防止 | ⚠️ 部分防止 | MySQL InnoDB 默认(间隙锁防幻读) |
| SERIALIZABLE | ✅ 防止 | ✅ 防止 | ✅ 防止 | 最高,串行执行,性能差 |
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;MVCC(多版本并发控制)
InnoDB 通过 MVCC 在不加锁的情况下实现快照读,大幅提升并发性能。
三个核心组件
1. 隐藏字段(每行数据)
| 字段 | 说明 |
|---|---|
DB_TRX_ID | 最近一次修改该行的事务 ID(单调递增) |
DB_ROLL_PTR | 指向 undo log 链表中上一个版本的指针 |
DB_ROW_ID | 无主键时自动生成的行 ID |
2. undo log 版本链
每次 UPDATE 不直接覆盖,而是将旧版本写入 undo log,形成链表:
当前行(trx_id=100)→ undo(trx_id=80) → undo(trx_id=50) → ...
3. ReadView(快照)
事务开始时生成 ReadView,记录:
m_ids:当前活跃(未提交)的事务 ID 列表min_trx_id:活跃事务最小 IDmax_trx_id:下一个待分配的事务 ID
可见性判断规则(对版本链中每个版本):
if trx_id < min_trx_id:
→ 该版本在 ReadView 生成前已提交 → 可见
elif trx_id >= max_trx_id:
→ 该版本在 ReadView 生成后开启 → 不可见,找旧版本
elif trx_id in m_ids:
→ 该版本是活跃事务(未提交) → 不可见,找旧版本
else:
→ 在 ReadView 创建时已提交 → 可见
RC 与 RR 的差异
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| RC(读已提交) | 每次 SELECT 都生成新快照 | 可读到其他事务提交的最新数据 |
| RR(可重复读) | 事务第一次读时生成,之后复用 | 整个事务期间看到的是同一快照,不可重复读不存在 |
锁机制
锁粒度
| 锁类型 | 说明 | 适用 |
|---|---|---|
| 表锁(Table Lock) | 锁整张表,并发低 | MyISAM,DDL |
| 行锁(Row Lock) | 锁具体行,并发高 | InnoDB DML |
| 间隙锁(Gap Lock) | 锁索引记录间的间隙,防幻读 | InnoDB RR |
| 临键锁(Next-Key Lock) | 行锁 + 间隙锁的组合 | InnoDB RR 默认 |
| 意向锁(Intention Lock) | 表级锁,标记”某行已加行锁”,防止表锁与行锁冲突 | InnoDB 自动 |
S 锁 / X 锁兼容矩阵
| S 锁(共享锁) | X 锁(排他锁) | |
|---|---|---|
| S 锁 | ✅ 兼容 | ❌ 冲突 |
| X 锁 | ❌ 冲突 | ❌ 冲突 |
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE; -- S 锁(读锁)
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- X 锁(写锁)死锁
两个事务互相等待对方持有的锁:
T1: 锁 A → 等 B
T2: 锁 B → 等 A
InnoDB 自动检测死锁并回滚代价最小的事务(ERROR 1213: Deadlock found)。
预防死锁:
- 多个事务按相同顺序加锁
- 减少事务持锁时间,不在事务内做远程调用
- 大批量更新改为小批次
分布式事务
单机事务无法跨越多个数据库/服务,需要分布式事务协议。
XA(两阶段提交,2PC)
Coordinator(协调者)
│
│ Phase 1:PREPARE
├──► Participant A(prepare → vote YES/NO)
└──► Participant B(prepare → vote YES/NO)
│
│ Phase 2:COMMIT or ROLLBACK
├──► Participant A
└──► Participant B
优点:强一致性,ACID 保证
缺点:协调者单点故障,参与者长时间锁定资源,性能差;同步阻塞
-- MySQL XA 示例
XA START 'xid1';
UPDATE account SET balance = balance - 100 WHERE id = 1;
XA END 'xid1';
XA PREPARE 'xid1';
-- 所有参与者 PREPARE 成功后
XA COMMIT 'xid1';SAGA 模式
将长事务拆分为多个本地事务,每步都有对应的补偿事务:
T1 → T2 → T3 → 成功
T1 → T2 → T3 失败 → C3 → C2 → C1(补偿回滚)
- 优点:无长时锁,高可用,适合微服务
- 缺点:实现复杂,存在中间状态(最终一致)
- 实现方式:编排(Orchestration,由 Saga Coordinator 协调)或 编舞(Choreography,各服务通过事件通信)
TCC 模式(Try-Confirm-Cancel)
| 阶段 | 说明 |
|---|---|
| Try | 预留资源(冻结库存、预扣金额),不真正执行 |
| Confirm | 正式提交(扣减库存、扣款完成) |
| Cancel | 释放预留资源(解冻库存、退款) |
优点:最终一致,业务灵活
缺点:业务侵入性强,每个接口需实现三个方法;需处理幂等和空回滚
本地消息表
业务 DB 写入 → 本地消息表(同一事务)
↓
定时任务扫描消息表
↓
发送到 MQ → 消费者处理 → 更新消息状态
优点:实现简单,可靠性高
缺点:消息表是性能瓶颈,与业务 DB 强耦合
方案选型
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| XA/2PC | 强 | 低 | 低 | 数据库内部,少量参与者 |
| TCC | 最终 | 高 | 高 | 核心支付,资金场景 |
| SAGA | 最终 | 高 | 中 | 长流程业务(订单、物流) |
| 本地消息表 | 最终 | 中 | 低 | 跨服务异步通知 |
| 最大努力通知 | 弱 | 高 | 低 | 通知类、对账容忍 |
Spring @Transactional 传播行为
| 传播行为 | 说明 | 典型场景 |
|---|---|---|
REQUIRED(默认) | 有事务则加入,没有则新建 | 绝大多数 Service 方法 |
REQUIRES_NEW | 总是新建独立事务,挂起外层事务 | 记录审计日志(不随业务回滚) |
NESTED | 嵌套事务,外层回滚时内层也回滚,内层回滚不影响外层 | 批处理中单条可独立回滚 |
SUPPORTS | 有事务则加入,没有则以非事务方式执行 | 只读查询 |
NOT_SUPPORTED | 始终以非事务方式运行,挂起外层事务 | 批量查询,避免长事务 |
MANDATORY | 必须在事务中执行,否则抛异常 | 内部工具方法,强制调用方开事务 |
NEVER | 不允许在事务中执行,否则抛异常 | — |
常见坑
| 问题 | 原因 | 解决 |
|---|---|---|
@Transactional 不生效 | 同类内部方法调用(绕过代理) | 注入自身 Bean 或抽到独立类 |
| 事务不回滚 | 默认只回滚 RuntimeException,checked exception 不回滚 | 添加 rollbackFor = Exception.class |
| 长事务 | Service 方法持锁时间过长,包含 HTTP 调用、MQ 发送等 | 事务内只做 DB 操作,IO 操作移到事务外 |
| 读写分离失效 | 事务中的读操作路由到主库 | 只读事务用 @Transactional(readOnly = true) |
| 幂等性缺失 | MQ 消费者、定时任务重复执行写入 | 数据库唯一约束 + INSERT IGNORE |
相关
- MySQL — InnoDB 事务实现详解、锁与 MVCC
- PostgreSQL MVCC 与事务
- Spring Boot 事务管理 —
@Transactional详细用法 - Seata — 分布式事务框架(AT/TCC/SAGA/XA 模式)
- 数据传输 — 迁移过程中的事务一致性