TiDB 事务隔离级别

事务隔离级别是数据库事务处理的基础,ACID 中的 “I”,即 Isolation,指的就是事务的隔离性。

SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表:

Isolation LevelDirty WriteDirty ReadFuzzy ReadPhantom
READ UNCOMMITTEDNot PossiblePossiblePossiblePossible
READ COMMITTEDNot PossibleNot possiblePossiblePossible
REPEATABLE READNot PossibleNot possibleNot possiblePossible
SERIALIZABLENot PossibleNot possibleNot possibleNot possible

TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 ANSI 可重复读隔离级别MySQL 可重复读隔离级别

隔离级别 - 图1

注意

在 TiDB v3.0 中,事务的自动重试功能默认为禁用状态。不建议开启自动重试功能,因为可能导致事务隔离级别遭到破坏。更多关于事务自动重试的文档说明,请参考事务重试

从 TiDB v3.0.8 版本开始,新创建的 TiDB 集群会默认使用悲观事务模式,悲观事务中的当前读(for update 读)为不可重复读,关于悲观事务使用注意事项,请参考悲观事务模式

可重复读隔离级别 (Repeatable Read)

当事务隔离级别为可重复读时,只能读到该事务启动时已经提交的其他事务修改的数据,未提交的数据或在事务启动后其他事务提交的数据是不可见的。对于本事务而言,事务语句可以看到之前的语句做出的修改。

对于运行于不同节点的事务而言,不同事务启动和提交的顺序取决于从 PD 获取时间戳的顺序。

处于可重复读隔离级别的事务不能并发的更新同一行,当事务提交时发现该行在该事务启动后,已经被另一个已提交的事务更新过,那么该事务会回滚。示例如下:

  1. create table t1(id int);
  2. insert into t1 values(0);
  3. start transaction; | start transaction;
  4. select * from t1; | select * from t1;
  5. update t1 set id=id+1; | update t1 set id=id+1; -- 如果使用悲观事务,则后一个执行的 update 语句会等锁,直到持有锁的事务提交或者回滚释放行锁。
  6. commit; |
  7. | commit; -- 事务提交失败,回滚。如果使用悲观事务,可以提交成功。

与 ANSI 可重复读隔离级别的区别

尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 A Critique of ANSI SQL Isolation Levels 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。

与 MySQL 可重复读隔离级别的区别

MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 使用乐观事务时会导致事务回滚,导致事务最终失败,而 TiDB 默认的悲观事务和 MySQL 是可以更新成功的。

读已提交隔离级别 (Read Committed)

从 TiDB v4.0.0-beta 版本开始,TiDB 支持使用 Read Committed 隔离级别。由于历史原因,当前主流数据库的 Read Committed 隔离级别本质上都是 Oracle 定义的一致性读隔离级别。TiDB 为了适应这一历史原因,悲观事务中的 Read Committed 隔离级别的实质行为也是一致性读。

隔离级别 - 图2

注意

Read Committed 隔离级别仅在悲观事务模式下生效。在乐观事务模式下设置事务隔离级别为 Read Committed 将不会生效,事务将仍旧使用可重复读隔离级别。

从 v6.0.0 版本开始,TiDB 支持使用系统变量 tidb_rc_read_check_ts 对读写冲突较少情况下优化时间戳的获取。开启此变量后,SELECT 语句会尝试使用前一个有效的时间戳进行数据读取,初始值为事务的 start_ts

  • 如果整个读取过程没有遇到更新的数据版本,则返回结果给客户端且 SELECT 语句执行成功。
  • 如果读取过程中遇到更新的数据版本:
    • 如果当前 TiDB 尚未向客户端回复数据,则尝试重新获取一个新的时间戳重试此语句。
    • 如果 TiDB 已经向客户端返回部分数据,则 TiDB 会向客户端报错。每次向客户端回复的数据量受 tidb_init_chunk_sizetidb_max_chunk_size 控制。

在使用 READ-COMMITTED 隔离级别且单个事务中 SELECT 语句较多、读写冲突较少的场景,可通过开启此变量来避免获取全局 timestamp 带来的延迟和开销。

从 v6.3.0 版本开始,TiDB 支持通过开启系统变量 tidb_rc_write_check_ts 对点写冲突较少情况下优化时间戳的获取。开启此变量后,点写语句会尝试使用当前事务有效的时间戳进行数据读取和加锁操作,且在读取数据时按照开启 tidb_rc_read_check_ts 的方式读取数据。目前该变量适用的点写语句包括 UPDATEDELETESELECT ...... FOR UPDATE 三种类型。点写语句是指将主键或者唯一键作为过滤条件且最终执行算子包含 POINT-GET 的写语句。目前这三种点写语句的共同点是会先根据 key 值做点查,如果 key 存在再加锁,如果不存在则直接返回空集。

  • 如果点写语句的整个读取过程中没有遇到更新的数据版本,则继续使用当前事务的时间戳进行加锁。
    • 如果加锁过程中遇到因时间戳旧而导致写冲突,则重新获取最新的全局时间戳进行加锁。
    • 如果加锁过程中没有遇到写冲突或其他错误,则加锁成功。
  • 如果读取过程中遇到更新的数据版本,则尝试重新获取一个新的时间戳重试此语句。

在使用 READ-COMMITTED 隔离级别且单个事务中点写语句较多、点写冲突较少的场景,可通过开启此变量来避免获取全局时间戳带来的延迟和开销。

与 MySQL Read Committed 隔离级别的区别

MySQL 的 Read Committed 隔离级别大部分符合一致性读特性,但其中存在某些特例,如半一致性读 (semi-consistent read),TiDB 没有兼容这个特殊行为。

更多阅读