数据库事务

返回 关系型数据库

事务(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:活跃事务最小 ID
  • max_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

相关