TiDB 事务机制理解

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 三大核心组件协同

  1. PD(Placement Driver):全局授时器 TSO,事务开启分配 start_ts(快照版本号),提交分配 commit_ts
  2. TiDB Server:接收SQL、生成执行计划、驱动两阶段提交 2PC;
  3. TiKV:分布式存储,三层列簇实现 MVCC、锁管理、多版本数据存储:
    • Default CF:存储数据多版本(key + start_ts 作为版本主键)
    • Lock CF:存储事务锁(悲观锁/乐观预写锁)
    • Write CF:记录数据提交版本映射(可见性判断核心)

2.2 Percolator 两阶段提交 2PC 流程(所有事务通用)

  1. 阶段1:Prewrite 预提交
    • TiDB 从 PD 获取 start_ts,标记当前事务快照;
    • 选定其中一条修改行作为 Primary Key(主锁,事务核心标识);
    • 向所有涉及的 TiKV 分片发送预写请求,写入 Lock CF 上锁、Default CF 写入新版本数据;
    • 上锁冲突直接返回失败,乐观事务直接报错,悲观事务阻塞等待锁释放;
  2. 阶段2:Commit 提交
    • 全部预写成功后,向 PD 申请全局递增 commit_ts
    • 仅向 Primary Key 所在 TiKV 发送 Commit 请求,写入 Write CF 标记提交版本;
    • 异步清理所有分片锁,事务对外可见;
    • 任意分片预写失败,全部分片回滚删除预写数据。

2.3 MVCC 快照读核心规则

TiDB 所有普通 SELECT(快照读)不加锁,只会读取 commit_ts < 当前事务start_ts 的已提交数据:

  1. 悲观事务下,快照读无视其他事务悲观锁,直接读取历史稳定版本;
  2. 只有 SELECT ... FOR UPDATE 当前读才会加锁读取最新未提交数据;
  3. 乐观事务快照读若检测到预写锁,会报 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

实操结论

  1. TiDB 默认 RR 隔离底层是 SI 快照隔离,杜绝脏读、不可重复读
  2. 普通快照读不加锁,读写不阻塞,高并发读性能远优于 MySQL InnoDB;
  3. 事务快照在 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 关键差异(实操踩坑总结)

  1. 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会阻塞)
    
  2. 乐观事务冲突时机不同
    MySQL 主键冲突即时报错;TiDB乐观事务主键冲突延迟到Commit才报错,整事务回滚。
  3. 读写阻塞模型
    MySQL 加锁后快照读同样阻塞;TiDB悲观锁仅阻塞写,普通快照读完全无阻塞。

十、生产落地选型建议(结合实操结论)

  1. 金融、库存、高并发扣减场景:使用悲观事务,兼容MySQL,避免业务处理冲突重试逻辑;
  2. 离线报表、ETL、低并发后台任务:使用乐观事务,无锁开销,提升集群吞吐;
  3. 实时大屏、实时同步:切换为 READ COMMITTED 隔离,读取最新数据;
  4. 批量超大事务规避:单事务写入行数建议控制在万级以内,过大预写锁会导致集群锁竞争、内存飙升;
  5. 乐观事务业务适配:代码捕获 Write conflict 错误,增加循环重试逻辑。
1 个赞

一般都是用乐观锁,业务实现

建议先确认一下你的TiDB版本,不同版本默认事务模式有差异。实操时可以在两个会话中同时执行以下测试:

  1. 验证隔离级别:Session A执行BEGIN; UPDATE account SET balance=balance-100 WHERE id=1;,不提交;Session B查询SELECT * FROM account WHERE id=1;,看是否读到脏数据(SI下读不到未提交数据)。

TiDB 用 TSO+MVCC,跨 TiKV 走 2PC;乐观适合冲突少,悲观适合高冲突写。建议按官方文档做写冲突与隔离实验加深理解。

1 个赞

这篇总结的事务知识比较到位啊

业务能实验

默认隔离级别感觉不太行,我们都调成rc的了

哇哦,写的很好