Skip to content

TiDB 事务模型——乐观事务 vs 悲观事务

⭐⭐⭐TiDB 分布式优化TiDB事务乐观锁悲观锁Write Conflict重试

迁移后偶发 Write conflict:tidb_txn_mode 没设置

应用从 MySQL 迁移到 TiDB 后,偶发报错 ERROR 9007 (HY000): Write conflictInformation schema is changed,业务侧出现订单重复、余额不一致等数据问题。排查发现迁移后未设置 tidb_txn_mode,默认行为与预期不符——早期 TiDB 默认乐观事务,TiDB 6.0+ 默认悲观事务,中间版本升级可能导致行为突变。

sql
-- 乐观事务模式下:两个会话同时修改同一行
-- 会话A
SET SESSION tidb_txn_mode = 'optimistic';
BEGIN;
UPDATE t_account SET balance = balance - 1000 WHERE id = 1;
-- 此时不持锁,其他事务可同时写入

-- 会话B(同时执行)
SET SESSION tidb_txn_mode = 'optimistic';
BEGIN;
UPDATE t_account SET balance = balance - 500 WHERE id = 1;
COMMIT;  -- 事务B先提交成功

-- 回到会话A
COMMIT;  -- ❌ Write Conflict! 事务A提交失败

真实场景

余额扣减、库存扣减、订单状态变更——这些"写后读"场景是 Write Conflict 的高发区。乐观事务在提交阶段才检测冲突,提交失败意味着整个事务白做;悲观事务在写入时加锁,与经典 MySQL 行为一致。了解两种模式的差异与选择策略,是从 MySQL 平滑迁移到 TiDB 的核心前置知识。

问题分析

bad.sql

sql
-- bad.sql: 乐观事务模式下的 Write Conflict

-- 设置乐观事务模式
SET SESSION tidb_txn_mode = 'optimistic';

-- 事务A (模拟第一个会话)
BEGIN;
UPDATE t_account SET balance = balance - 1000 WHERE id = 1;
-- 此时不持有锁,其他事务可以同时修改同一行

-- 事务B (模拟第二个会话——在另一个终端执行)
-- BEGIN;
-- UPDATE t_account SET balance = balance - 500 WHERE id = 1;  -- 乐观模式下不会阻塞
-- COMMIT;  -- 事务B先提交成功

-- 回到事务A
-- COMMIT;  -- Write Conflict! 事务A的提交被拒绝,需要客户端重试

-- 查看乐观事务的重试限制
SHOW VARIABLES LIKE 'tidb_retry_limit';

-- 查看当前事务模式
SELECT @@tidb_txn_mode;

为什么会出现 Write Conflict

乐观事务的执行时间线:

时间线   事务A                            事务B
  T1     BEGIN;                           BEGIN;
  T2     UPDATE id=1 (本地缓存写入)         UPDATE id=1 (本地缓存写入)
  T3                                       COMMIT;  ← 先到 TiKV Prewrite
  T4     COMMIT;  ← ❌ Prewrite 阶段检测到
           id=1 版本已被事务B修改          → Write Conflict!
         → 事务A 被拒绝

乐观事务基于 TiKV 的 Percolator 模型:

  • 事务执行期间,所有写入缓存在 TiDB 内存中,不写 TiKV
  • Prewrite 阶段对比 start_ts 版本号与行当前最新版本
  • 若发现冲突(行已被其他事务修改),整个事务失败

冲突检测在提交阶段,而非写入阶段——这是乐观事务 Write Conflict 的根因。

乐观事务 vs 悲观事务冲突处理对比:

        乐观事务                      悲观事务
写入时   无锁,不检查冲突              加锁,冲突则等待
提交时   Prewrite 检测冲突             直接提交(锁已保证无冲突)
冲突     Write Conflict 报错           锁等待超时报错
恢复     客户端重试整个事务             自动获取锁后继续
维度乐观事务(bad)悲观事务(good)
写入时无锁加锁(行级)
冲突处理提交时检测,失败重试写入时等待/超时
并发写入同一行不会阻塞,但先提交者胜串行执行
死锁风险极低低(TiDB 有死锁检测)
客户端复杂度需实现重试逻辑无需特殊处理
适合场景低冲突 + 高并发高冲突 OLTP

优化方案

good.sql

sql
-- good.sql: 悲观事务模式——与 MySQL 行为一致

-- 设置悲观事务模式(TiDB 6.0+ 默认)
SET SESSION tidb_txn_mode = 'pessimistic';

-- 事务A (模拟第一个会话)
BEGIN;
UPDATE t_account SET balance = balance - 1000 WHERE id = 1;
-- 悲观模式下此行已被锁定,其他事务必须等待

-- 事务B (模拟第二个会话——在另一个终端执行)
-- BEGIN;
-- UPDATE t_account SET balance = balance - 500 WHERE id = 1;  
-- 阻塞等待事务A释放锁(默认 innodb_lock_wait_timeout = 50s)
-- COMMIT;

-- 事务A提交后事务B继续执行
-- COMMIT;  -- 成功

-- 事务选择对比
SELECT '悲观事务' AS mode, '写入时加锁,提交即成功' AS behavior
UNION ALL
SELECT '乐观事务', '提交时检测冲突,失败需重试';

-- AS OF TIMESTAMP 历史读(TiDB 特有功能)
SELECT @@tidb_snapshot;
-- SET @@tidb_snapshot = '2026-07-01 00:00:00';
-- SELECT * FROM t_account;  -- 读取历史快照
-- SET @@tidb_snapshot = '';

原理

悲观事务在 DML 执行阶段就向 TiKV 获取锁,UPDATE 立即锁定目标行,其他事务必须等待。与 MySQL InnoDB 完全一致——提交即成功,无 Write Conflict。

悲观事务的执行时间线:

时间线   事务A                            事务B
  T1     BEGIN;                           BEGIN;
  T2     UPDATE id=1 → 加行锁              UPDATE id=1 → ⏳ 等待锁
  T3     COMMIT;  → 释放锁
  T4                                      获取锁,继续执行
  T5                                      COMMIT;  ✅ 成功

悲观事务的锁机制:

  • Prewrite 阶段:向 TiKV 写入数据并加锁
  • Commit 阶段:写入提交记录,释放锁
  • 锁粒度:行级锁(不支持 Gap Lock)
  • 死锁检测:TiDB Leader 节点定期检测死锁并回滚
  • 锁等待超时innodb_lock_wait_timeout 默认 50 秒

对比

乐观事务(冲突率低)乐观事务(冲突率高)悲观事务
写入阻塞有(锁等待)
提交失败率极低极低
重试开销重试爆炸
并发吞吐最高中等
实现复杂度需重试逻辑需重试+退避逻辑与 MySQL 一致
指标优化前 (bad)优化后 (good)
访问类型--
使用索引--
扫描行数--
附加信息--

🚀 从提交阶段冲突检测变为写入阶段加锁,消除 Write Conflict,与 MySQL 行为完全一致

避坑指南

注意事项

  1. 版本差异:TiDB 3.0 之前只支持乐观事务,3.0+ 引入悲观事务(实验),4.0 GA,6.0+ 默认悲观。升级时检查 tidb_txn_mode

  2. 不要混用:同一应用尽量统一事务模型,避免连接池中存在不同 tidb_txn_mode 的会话。

  3. 乐观事务需重试:使用乐观模式必须在应用层实现重试逻辑(捕获 Write conflict 错误 + 指数退避)。

  4. 长事务慎用悲观:悲观事务持有锁时间长,可能阻塞其他事务,大事务应拆分为小事务或改用乐观模式。

  5. 死锁不会丢数据:TiDB 会自动回滚死锁事务中的较小者,应用捕获死锁错误并重试即可。

  6. 历史读是利器AS OF TIMESTAMP 可实现无锁的一致性快照读,适合报表和历史数据查询。

  7. 监控 Write Conflict:通过 TiDB Dashboard 或 INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY 监控冲突率。

乐观 vs 悲观事务选型表

场景推荐模式理由
MySQL 迁移应用悲观(默认)与 MySQL 行为一致,无需改代码
高冲突 OLTP(秒杀、扣减)悲观锁等待优于重试爆炸
低冲突高并发(日志写入、事件记录)乐观无锁等待,吞吐更高
长事务(多表关联更新)悲观避免全部重试
读多写少(报表、分析)乐观几乎不冲突,极致性能
微服务分布式事务乐观避免分布式锁,简化架构
历史数据查询任一 + AS OF TIMESTAMPTiDB 特有,无锁一致性快照

模式切换

方法作用域示例
全局变量新会话SET GLOBAL tidb_txn_mode = 'pessimistic'
会话变量当前会话SET SESSION tidb_txn_mode = 'optimistic'
连接串参数JDBCjdbc:mysql:tidb://...?tidb_txn_mode=optimistic

TiDB 6.0+ 默认悲观

如果你用的是 TiDB 6.0 及以上版本,默认已是悲观事务。只需了解乐观事务的存在并在低冲突场景按需切换即可。如果从旧版 TiDB 升级,需要确认现有应用是否依赖乐观事务行为。

本地复现

bash
# 在 TiDB 上运行
./scripts/run-case.sh 85-tidb-transaction --ver tidb

# 跳过造数据重跑
./scripts/run-case.sh 85-tidb-transaction --ver tidb --no-seed

MIT Licensed