TiDB事务机制深入学习理解(含完整可复现实操)
TiDB 是分布式 NewSQL 数据库,兼容 MySQL 语法,但事务底层采用 Percolator 分布式事务模型+MVCC,分为乐观、悲观两套事务模式,隔离级别对外兼容 MySQL REPEATABLE READ,底层实际是快照隔离 SI。
一、前置实操环境准备(必做)
1. 环境说明
- 测试环境:TiDB Playground 单机集群(tidb-server + pd + tikv,本地一键启动,无硬件门槛)
- 客户端:MySQL 8.0 客户端 / Navicat / DBeaver,开两个会话窗口(Session A、Session B)用于并发事务演示
- 测试表:账户转账表,模拟金融事务场景,所有实操共用该表
-- 1. 创建测试表
CREATE TABLE account (
id INT PRIMARY KEY COMMENT '账户ID',
balance DECIMAL(10,2) NOT NULL COMMENT '余额'
);
-- 2. 初始化数据
INSERT INTO account VALUES (1, 1000.00), (2, 1000.00);
-- 3. 查看初始数据
SELECT * FROM account;
执行结果:
| id | balance |
|---|---|
| 1 | 1000.00 |
| 2 | 1000.00 |
2. 事务基础语法速查(TiDB 兼容 MySQL)
-- 显式开启事务(悲观/乐观两种写法)
BEGIN; -- 默认悲观(v3.0.8后新集群默认)
BEGIN PESSIMISTIC; -- 强制悲观事务
BEGIN OPTIMISTIC; -- 强制乐观事务
COMMIT; -- 提交事务
ROLLBACK; -- 回滚事务
SET autocommit = 0; -- 关闭自动提交,等同于隐式事务
SET autocommit = 1; -- 开启自动提交(单条SQL自动提交)
二、TiDB 分布式事务底层核心原理(理论铺垫,配合实操理解)
2.1 三大核心组件协同
- PD(Placement Driver):全局授时器 TSO,事务开启分配
start_ts(快照版本号),提交分配commit_ts; - TiDB Server:接收SQL、生成执行计划、驱动两阶段提交 2PC;
- TiKV:分布式存储,三层列簇实现 MVCC、锁管理、多版本数据存储:
- Default CF:存储数据多版本(key + start_ts 作为版本主键)
- Lock CF:存储事务锁(悲观锁/乐观预写锁)
- Write CF:记录数据提交版本映射(可见性判断核心)
2.2 Percolator 两阶段提交 2PC 流程(所有事务通用)
- 阶段1:Prewrite 预提交
- TiDB 从 PD 获取
start_ts,标记当前事务快照; - 选定其中一条修改行作为
Primary Key(主锁,事务核心标识); - 向所有涉及的 TiKV 分片发送预写请求,写入 Lock CF 上锁、Default CF 写入新版本数据;
- 上锁冲突直接返回失败,乐观事务直接报错,悲观事务阻塞等待锁释放;
- TiDB 从 PD 获取
- 阶段2:Commit 提交
- 全部预写成功后,向 PD 申请全局递增
commit_ts; - 仅向 Primary Key 所在 TiKV 发送 Commit 请求,写入 Write CF 标记提交版本;
- 异步清理所有分片锁,事务对外可见;
- 任意分片预写失败,全部分片回滚删除预写数据。
- 全部预写成功后,向 PD 申请全局递增
2.3 MVCC 快照读核心规则
TiDB 所有普通 SELECT(快照读)不加锁,只会读取 commit_ts < 当前事务start_ts 的已提交数据:
- 悲观事务下,快照读无视其他事务悲观锁,直接读取历史稳定版本;
- 只有
SELECT ... FOR UPDATE当前读才会加锁读取最新未提交数据; - 乐观事务快照读若检测到预写锁,会报
Key is locked等待重试。
三、实操1:基础ACID事务验证(转账原子性、一致性)
场景:账户1转账200元到账户2,事务要么全部成功,要么全部回滚
Session A 完整事务(正常提交)
BEGIN;
UPDATE account SET balance = balance - 200 WHERE id = 1;
UPDATE account SET balance = balance + 200 WHERE id = 2;
-- 此时Session B查询看不到未提交修改(MVCC快照隔离)
COMMIT;
提交后查询结果:
| id | balance |
|---|---|
| 1 | 800.00 |
| 2 | 1200.00 |
场景2:事务中间异常回滚验证
BEGIN;
UPDATE account SET balance = balance - 200 WHERE id = 1;
-- 模拟异常,手动回滚
ROLLBACK;
-- 查询数据不变,原子性生效
SELECT * FROM account;
验证结论:TiDB 分布式事务满足原子性(Atomic),跨分片修改要么全生效,要么全部撤销。
四、实操2:MVCC 快照读隔离验证(无脏读、可重复读)
操作步骤(分两个会话并发执行)
Session A(事务A,不提交)
BEGIN;
UPDATE account SET balance = 0 WHERE id = 1;
-- 暂停,不执行commit
SELECT * FROM account WHERE id = 1; -- 本事务内可见修改:0.00
Session B(事务B,并发快照读)
BEGIN;
SELECT * FROM account WHERE id = 1; -- 读取快照版本,依旧是800.00,无脏读
回到Session A执行提交
COMMIT;
Session B 再次查询
SELECT * FROM account WHERE id = 1; -- 依旧读取事务启动时快照,还是800.00(可重复读)
Session B 关闭当前事务,新开事务查询
COMMIT;
BEGIN;
SELECT * FROM account WHERE id = 1; -- 读取最新已提交0.00
实操结论
- TiDB 默认 RR 隔离底层是 SI 快照隔离,杜绝脏读、不可重复读;
- 普通快照读不加锁,读写不阻塞,高并发读性能远优于 MySQL InnoDB;
- 事务快照在
BEGIN瞬间固定,事务全程不会看到其他事务新提交数据。
五、实操3:乐观事务 vs 悲观事务 核心冲突对比(重点实操)
5.1 乐观事务:冲突在Commit阶段报错,无阻塞
乐观事务核心逻辑:全程不加锁,提交时校验写写冲突,冲突直接回滚。
并发演示(两个会话同时修改同一行)
Session A:
BEGIN OPTIMISTIC; -- 强制乐观事务
UPDATE account SET balance = balance + 100 WHERE id = 1;
-- 暂停,不提交
Session B:
BEGIN OPTIMISTIC;
UPDATE account SET balance = balance + 200 WHERE id = 1;
COMMIT; -- Session B先提交成功
回到Session A执行提交:
COMMIT;
报错信息:Write conflict
原因:事务A启动后,该行已被事务B修改并提交,
commit_ts > A.start_ts,预写阶段检测写写冲突,事务整体回滚。
查询表数据:id=1 仅增加200,事务A修改全部失效。
5.2 悲观事务:DML执行时加锁,冲突阻塞等待(TiDB默认模式)
悲观事务核心逻辑:执行UPDATE/SELECT FOR UPDATE 立即加悲观行锁,其他修改事务阻塞等待锁释放。
Session A:
BEGIN PESSIMISTIC;
UPDATE account SET balance = balance + 100 WHERE id = 1; -- 立刻加悲观锁
-- 暂停不提交
Session B:
BEGIN PESSIMISTIC;
UPDATE account SET balance = balance + 200 WHERE id = 1;
-- 语句阻塞,等待锁(默认等待50秒)
Session A执行提交:
COMMIT; -- 释放悲观锁
Session B阻塞结束,自动执行更新并提交,最终余额叠加两次修改。
5.3 悲观锁快照读不阻塞验证(TiDB独有特性)
Session A 持有悲观锁不提交:
BEGIN PESSIMISTIC;
UPDATE account SET balance = 999 WHERE id = 1;
Session B 普通快照读(不加锁):
SELECT * FROM account WHERE id = 1; -- 瞬间返回历史数据,不阻塞
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 当前读,阻塞等待锁
关键区别MySQL:MySQL InnoDB RR下快照读同样不加锁,但TiDB悲观锁读写完全隔离,读业务不受写锁阻塞。
5.4 两种事务选型实操总结
| 维度 | 乐观事务 | 悲观事务(默认) |
|---|---|---|
| 加锁时机 | Commit预写阶段才上锁 | DML执行立即加锁 |
| 冲突表现 | Commit直接报错回滚 | 写操作阻塞等待锁 |
| 适用场景 | 低并发写入、报表、离线任务 | 高并发修改、金融交易、库存扣减 |
| 应用改造 | 必须捕获Write conflict重试 | 兼容MySQL业务,无需改造 |
六、实操4:锁超时、死锁真实场景复现
6.1 悲观锁等待超时(默认50秒)
参数控制:innodb_lock_wait_timeout,单位秒
Session A:
BEGIN PESSIMISTIC;
UPDATE account SET balance=1 WHERE id=1;
-- 长时间不提交,持有锁超过50秒
Session B执行修改:
UPDATE account SET balance=2 WHERE id=1;
等待超时后抛出错误:Error 1205: Lock wait timeout exceeded
6.2 悲观事务死锁自动检测复现
Session A:
BEGIN PESSIMISTIC;
UPDATE account SET balance=1 WHERE id=1; -- 持有id=1锁
-- 暂停
Session B:
BEGIN PESSIMISTIC;
UPDATE account SET balance=2 WHERE id=2; -- 持有id=2锁
-- 暂停
Session A继续执行:
UPDATE account SET balance=1 WHERE id=2; -- 请求id=2锁,阻塞
Session B继续执行:
UPDATE account SET balance=2 WHERE id=1; -- 请求id=1锁,形成循环等待
TiDB 自动检测死锁,随机终止其中一个事务,报错 Error 1213: Deadlock found。
七、实操5:隔离级别切换 Read Committed 读已提交
TiDB 支持两种隔离级别:REPEATABLE READ(默认SI)、READ COMMITTED
切换语法
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 全局生效
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
RC 隔离实操验证(出现不可重复读)
Session A(RC隔离):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id=1; -- 读取800
-- 暂停
Session B提交修改:
BEGIN PESSIMISTIC;
UPDATE account SET balance=1500 WHERE id=1;
COMMIT;
Session A再次查询:
SELECT balance FROM account WHERE id=1; -- 读取最新1500,不可重复读
RC 隔离每次快照读都会读取当前最新已提交版本,适合实时报表、数据同步场景。
八、实操6:生产环境事务优化参数调优(可直接落地)
1. 乐观事务重试控制
-- 开启自动重试(仅乐观事务生效,默认关闭)
SET tidb_disable_txn_auto_retry = 0;
-- 最大重试次数
SET tidb_retry_limit = 10;
2. 异步提交加速2PC(v5.0+默认开启)
-- 预写完成立即返回客户端,commit阶段异步执行,提升吞吐
SET tidb_enable_async_commit = 1;
3. 悲观锁等待超时调整
SET innodb_lock_wait_timeout = 30; -- 缩短锁等待时长,快速失败
4. 全局默认事务模式切换
SET GLOBAL tidb_txn_mode = 'pessimistic'; -- 全局默认悲观
SET GLOBAL tidb_txn_mode = 'optimistic'; -- 全局默认乐观
九、TiDB 事务与 MySQL InnoDB 关键差异(实操踩坑总结)
- RR隔离底层实现不同
MySQL InnoDB RR依靠行锁+间隙锁防幻读;TiDB RR是快照隔离SI,无间隙锁,范围查询不会阻塞插入。
实操验证:CREATE TABLE t (id INT PRIMARY KEY); INSERT INTO t VALUES(1,5,10); BEGIN PESSIMISTIC; SELECT * FROM t WHERE id BETWEEN 1 AND 10 FOR UPDATE; -- 另一个会话执行 INSERT INTO t VALUES(6); 不会阻塞(MySQL会阻塞) - 乐观事务冲突时机不同
MySQL 主键冲突即时报错;TiDB乐观事务主键冲突延迟到Commit才报错,整事务回滚。 - 读写阻塞模型
MySQL 加锁后快照读同样阻塞;TiDB悲观锁仅阻塞写,普通快照读完全无阻塞。
十、生产落地选型建议(结合实操结论)
- 金融、库存、高并发扣减场景:使用悲观事务,兼容MySQL,避免业务处理冲突重试逻辑;
- 离线报表、ETL、低并发后台任务:使用乐观事务,无锁开销,提升集群吞吐;
- 实时大屏、实时同步:切换为
READ COMMITTED隔离,读取最新数据; - 批量超大事务规避:单事务写入行数建议控制在万级以内,过大预写锁会导致集群锁竞争、内存飙升;
- 乐观事务业务适配:代码捕获
Write conflict错误,增加循环重试逻辑。