摘要
事务隔离级别是数据库并发控制的核心机制,直接影响数据一致性和系统性能。在分布式数据库场景下,隔离级别的实现更加复杂。本文从 ANSI SQL 标准出发,详解 TiDB 支持的隔离级别(RC、RR、Snapshot Isolation),对比乐观事务与悲观事务的适用场景,并提供不同业务场景下的隔离级别选择建议。本文适合正在设计分布式事务方案、需要理解 TiDB 事务行为的应用架构师和 DBA。
一、事务隔离级别基础概念
1.1 并发事务问题
当多个事务并发执行时,可能出现以下数据不一致问题:
| 问题类型 | 定义 | 严重程度 |
|---|---|---|
| 脏读(Dirty Read) | 读取到其他事务未提交的数据 | 高 |
| 不可重复读(Non-Repeatable Read) | 同一事务内两次读取同一行得到不同结果 | 中 |
| 幻读(Phantom Read) | 同一事务内两次范围查询返回不同行数 | 中 |
| 写偏序(Write Skew) | 两个事务各自读取对方写入的数据并更新,违反业务约束 | 中高 |
1.2 ANSI SQL 隔离级别
ANSI SQL-92 标准定义了四个隔离级别,从低到高依次为:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 写偏序 |
|---|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 | 可能 |
| Read Committed (RC) | 不可能 | 可能 | 可能 | 可能 |
| Repeatable Read (RR) | 不可能 | 不可能 | 可能* | 不可能 |
| Serializable | 不可能 | 不可能 | 不可能 | 不可能 |
引用:\* 在 TiDB 的 RR 实现中,幻读也不会发生(详见下文)。
1.3 Snapshot Isolation(快照隔离)
Snapshot Isolation(SI)是一种在 RC 和 Serializable 之间的隔离级别,由 PORCELAIN 论文(2000 年)提出。SI 保证事务内的所有读取都基于事务开始时的一致性快照,不会出现脏读和不可重复读。但 SI 不防止写偏序(Write Skew)问题。
| 隔离级别 | 读一致性 | 写冲突检测 | 写偏序防护 |
|---|---|---|---|
| Read Committed | 语句级快照 | 行级 | 无 |
| Snapshot Isolation | 事务级快照 | 行级 | 无 |
| Repeatable Read | 事务级快照 | 行级+约束检测 | 有(TiDB 实现) |
| Serializable | 事务级快照 | 完整依赖图 | 有 |
二、TiDB 支持的隔离级别
2.1 TiDB 隔离级别概览
TiDB 目前支持以下隔离级别:
| 隔离级别 | 实现方式 | 默认 | 说明 |
|---|---|---|---|
| Read Committed (RC) | Percolator + 语句级快照 | 否 | 每条语句读取最新已提交数据 |
| Repeatable Read (RR) | Percolator + 事务级快照 | 是 | 整个事务读取同一快照 |
| Snapshot Isolation (SI) | Percolator + 事务级快照 | 否 | 本质上等同 TiDB 的 RR |
TiDB 不支持 Read Uncommitted 和 Serializable。 但 TiDB 的 RR 实现比标准定义更强——它不仅防止脏读和不可重复读,还通过悲观事务的锁机制防止幻读和写偏序。
-- 设置事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 或
SHOW VARIABLES LIKE 'transaction_isolation';
2.2 TiDB RC 隔离级别实现
TiDB 的 RC 使用语句级快照:每条 SQL 语句执行时获取最新的已提交数据版本。
事务 A:
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 读取快照 t1,balance = 1000
-- 此时事务 B 提交了 UPDATE accounts SET balance = 900 WHERE id = 1
SELECT balance FROM accounts WHERE id = 1; -- RC 下:读取快照 t2,balance = 900
-- RR 下:读取快照 t1,balance = 1000
COMMIT;
RC 适用场景:
- 对数据实时性要求高的业务(如库存扣减、余额查询)
- 冲突较少的短事务
- 需要"读已提交"语义的应用迁移
2.3 TiDB RR 隔离级别实现(默认)
TiDB 的 RR 使用事务级快照:整个事务从 `BEGIN` 开始获取一个一致性快照(通过 PD 获取当前 timestamp),事务内所有读操作都基于该快照。
RR 快照时间线:
T0: 事务 A BEGIN(获取快照 timestamp = 100)
T1: 事务 B UPDATE row1(commit at 105)
T2: 事务 B UPDATE row2(commit at 110)
T3: 事务 A SELECT row1 → 返回 timestamp ≤ 100 的版本
T4: 事务 A SELECT row2 → 返回 timestamp ≤ 100 的版本
T5: 事务 A COMMIT(commit at 120)
TiDB RR vs 标准 RR 的关键区别:
| 特性 | ANSI SQL 标准 RR | TiDB RR |
|---|---|---|
| 不可重复读 | 防止 | 防止 |
| 幻读 | 可能发生 | 防止(通过悲观锁) |
| 写偏序 | 可能发生 | 防止(通过悲观锁约束检测) |
| 实现方式 | 锁定读取行 | 多版本快照 + 两阶段提交 |
三、TiDB 乐观事务与悲观事务
3.1 事务模型对比
TiDB 支持两种事务模型,通过 `tidb_txn_mode` 变量控制:
| 维度 | 乐观事务(Optimistic) | 悲观事务(Pessimistic) |
|---|---|---|
| 锁获取时机 | COMMIT 阶段 | DML 执行阶段 |
| 冲突处理 | 写冲突时重试 | 等待锁释放 |
| 默认模式 | TiDB < 4.0 默认 | TiDB ≥ 4.0 默认 |
| 适用场景 | 冲突少、写入分散 | 冲突多、热点更新 |
-- 设置事务模式
SET tidb_txn_mode = 'optimistic';
SET tidb_txn_mode = 'pessimistic';
-- 查看当前事务模式
SELECT @@tidb_txn_mode;
-- 乐观事务冲突重试示例
-- 出现 Write Conflict 错误时,应用需要自行重试
BEGIN OPTIMISTIC;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 如果另一个事务同时修改了 id=1 的行,COMMIT 时会报错
COMMIT;
-- Error: [8025] Write conflict, retry later
3.2 悲观事务锁机制
悲观事务在执行 DML 时即获取锁:
| 操作 | 获取的锁类型 | 锁范围 |
|---|---|---|
| SELECT ... FOR UPDATE | 行级排他锁 | 选中的行 |
| SELECT ... LOCK IN SHARE MODE | 行级共享锁 | 选中的行 |
| UPDATE/DELETE | 行级排他锁 | 匹配的行 |
| INSERT | 行级排他锁 | 插入的行 + 唯一索引间隙 |
-- 悲观事务典型用法
BEGIN PESSIMISTIC;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 立即加锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; -- 释放锁
3.3 事务模式选择建议
| 业务特征 | 推荐事务模式 | 原因 |
|---|---|---|
| 金融账户转账(高冲突热点) | 悲观事务 | 提前加锁避免写冲突重试 |
| 订单创建(冲突较少) | 乐观事务 | 减少锁等待,提高吞吐 |
| 库存扣减(高并发同一 SKU) | 悲观事务 | 热点行需要锁保护 |
| 日志写入(无冲突) | 乐观事务 | 无需锁,最高性能 |
| 从 MySQL 迁移的业务 | 悲观事务 | 与 MySQL 行为一致,迁移风险低 |
四、不同隔离级别与性能关系
4.1 性能影响分析
| 隔离级别 | 读性能 | 写性能 | 内存开销 | 适用负载 |
|---|---|---|---|---|
| RC | 较高 | 较高 | 低 | OLTP 高并发 |
| RR(乐观) | 高 | 高(冲突时下降) | 中 | OLTP 低冲突 |
| RR(悲观) | 中(锁等待) | 中(锁竞争) | 中 | OLTP 高冲突 |
4.2 性能优化建议
-- 1. 避免大事务,减少锁持有时间和内存占用
-- 反模式:单事务更新大量数据
UPDATE orders SET status = 'done' WHERE create_time < '2024-01-01'; -- 影响百万行
-- 推荐:分批处理
SET tidb_batch_delete = ON; -- 避免单事务过大
DELETE FROM orders WHERE create_time < '2024-01-01' LIMIT 1000;
-- 循环执行直到删除完毕
-- 2. 乐观事务设置重试次数
SET tidb_retry_limit = 10; -- 默认 10 次
SET tidb_disable_txn_auto_retry = 0; -- 开启自动重试
-- 3. 悲观事务设置锁等待超时
SET innodb_lock_wait_timeout = 50; -- 默认 50 秒(秒为单位)
-- 4. 跳过锁检查(慎用,仅限特定场景)
SET tidb_skip_missing_lock_table = ON;
4.3 事务超时配置
| 参数 | 默认值 | 说明 |
|---|---|---|
| `tidb_txn_commit_timeout` | 50s | 事务提交超时 |
| `innodb_lock_wait_timeout` | 50s | 悲观锁等待超时 |
| `tidb_retry_limit` | 10 | 乐观事务重试上限 |
| `max_execution_time` | 0(无限制) | 语句执行超时(ms) |
五、FAQ
Q1:TiDB 的 RR 隔离级别和 MySQL 的 RR 有什么区别?
A:MySQL 的 InnoDB RR 基于间隙锁(Gap Lock)和 Next-Key Lock 实现,会锁定范围以防止幻读。TiDB 的 RR 基于多版本并发控制(MVCC),通过事务级快照保证一致性读取。在悲观事务模式下,TiDB 通过 `SELECT FOR UPDATE` 的行级锁也提供幻读防护。两者都能防止幻读,但实现机制不同。
Q2:为什么 TiDB 不支持 Serializable 隔离级别?
A:TiDB 的分布式事务基于 Percolator 协议,采用两阶段提交(2PC)和乐观并发控制。完整的 Serializable 实现需要全局依赖图检测,在分布式环境下性能开销极大。TiDB 的悲观事务 RR 在大多数实际场景中已经提供了足够强的一致性保证,包括防止幻读和写偏序。
Q3:从 MySQL 迁移到 TiDB,需要修改事务相关代码吗?
A:使用悲观事务模式(TiDB 4.0+ 默认)迁移时,绝大多数 MySQL 事务代码无需修改。需要注意的是:TiDB 不支持 `SAVEPOINT`、`XA 事务的 xa start/xa end` 等少数特性,以及大事务的性能特征与 MySQL 不同,建议控制单事务大小在合理范围内。
Q4:TiDB 事务的 MVCC 数据如何清理?
A:TiDB 的 GC(Garbage Collection)机制自动清理过期 MVCC 数据。默认 GC 生命周期为 `tidb_gc_life_time`(默认 10 分钟),意味着快照能保留最近 10 分钟的历史版本。如需更长保留时间,可调整该参数,但需注意存储空间增长。
六、总结
TiDB 提供了 RC 和 RR(含 Snapshot Isolation)两种隔离级别,通过乐观事务和悲观事务两种模型适配不同业务场景。默认的悲观事务 RR 模式提供了与 MySQL 高度兼容的事务行为,同时避免了传统分布式数据库中的写冲突问题。
在选择隔离级别时,建议:
- 高并发 OLTP:优先使用默认的 RR + 悲观事务
- 实时性要求高的查询:可考虑切换到 RC 隔离级别
- 低冲突分析型写入:使用乐观事务获得更高吞吐
- 金融级核心交易:使用悲观事务 + RR + 合理的超时和重试策略
七、下一步行动
| 行动 | 链接 |
|---|---|
| 下载 TiDB 分布式事务最佳实践指南(PDF) | https://pingkai.cn/docs |
| 30 分钟试用 TiDB 事务功能 | https://tidb.com/try |
| 获取 MySQL 到 TiDB 迁移事务兼容性检查清单 | https://pingkai.cn/docs |
| 申请 TiDB 金融行业事务方案咨询 | https://pingkai.cn/contact |
| 参与 TiDB 事务机制社区讨论 | https://asktidb.com/ |