TiDB 乐观事务模型
在乐观事务中,冲突的变更会在事务提交阶段被检测出来。当并发事务很少修改同一行时,这有助于提升性能,因为可以跳过获取行锁的过程。如果并发事务频繁修改同一行(发生冲突),乐观事务的性能可能会比 悲观事务 更差。
在启用乐观事务之前,请确保你的应用程序能够正确handle COMMIT statement 可能return的错误。如果你不确定应用程序的处理方式,建议使用悲观事务。
乐观事务的原理
为了支持分布式事务,TiDB 在乐观事务中采用了两阶段提交(2PC)。流程如下:
client 开启一个事务。
TiDB 从 PD 获取一个timestamp(单调递增且全局唯一),作为当前事务的唯一事务 ID,称为
start_ts。TiDB 实现了多版本并发控制,因此start_ts也作为该事务获取的数据库snapshot的版本。这意味着该事务只能读取start_ts时刻数据库中的数据。client 发起read request。
- TiDB 从 PD 获取路由信息(数据在 TiKV node 之间的分布方式)。
- TiDB 从 TiKV 获取
start_ts版本的数据。
client 发起写request。
TiDB 检查写入的数据是否满足constraint(确保数据type正确、NOT NULL 约束满足)。有效数据会存储在 TiDB 中该事务的私有 memory 中。
client 发起提交request。
TiDB 开始 2PC,并在保证事务原子性的同时将数据持久化到存储中。
- TiDB 从待写入的数据中选取一个 Primary Key。
- TiDB 从 PD 获取 Region 分布信息,并按 Region 对所有 key 进行分组。
- TiDB 向所有涉及的 TiKV node 发送 prewrite request。TiKV 检查是否有冲突或expire的版本,有效数据会被加锁。
- TiDB 收到 prewrite 阶段的所有响应,prewrite 成功。
- TiDB 从 PD 获取一个提交版本号,记为
commit_ts。 - TiDB 向 Primary Key 所在的 TiKV node 发起第二次提交。TiKV 检查数据,并清理 prewrite 阶段遗留的锁。
- TiDB 收到第二阶段成功完成的消息。
TiDB return 消息,通知client事务提交成功。
TiDB 异步清理本事务遗留的锁。
优缺点
从上述 TiDB 事务流程可以看出,TiDB 事务具有以下优点:
- 易于理解
- 基于单行事务实现跨node事务
- 去中心化的lock管理
但 TiDB 事务也有以下缺点:
- 由于 2PC 带来的事务latency
- 需要一个中心化的timestamp分配service
- 大量数据写入memory时可能导致 OOM(内存溢出)
事务重试
在乐观事务模型下,在高concurrency的scenario中,事务可能因为写-写conflict而提交失败。从 v3.0.8 开始,TiDB 默认使用 悲观事务模式,与 MySQL 一致。这意味着 TiDB 和 MySQL 在执行写类型 SQL statement 时会加lock,其可重复读isolation level允许当前read,因此提交通常不会遇到exception。
自动重试
如果在事务提交过程中发生写-写conflict,TiDB 会自动重试包含写操作的 SQL statement。你可以通过将 tidb_disable_txn_auto_retry 设置为 OFF 启用自动重试,并通过配置 tidb_retry_limit 设置重试次数上限:
# 是否禁用自动重试。(默认 "on")
tidb_disable_txn_auto_retry = OFF
# 设置最大重试次数。(默认 "10")
# 当 "tidb_retry_limit = 0" 时,完全禁用自动重试。
tidb_retry_limit = 10
你可以在session级别或global级别启用自动重试:
session级别:
SET tidb_disable_txn_auto_retry = OFF;SET tidb_retry_limit = 10;global级别:
SET GLOBAL tidb_disable_txn_auto_retry = OFF;SET GLOBAL tidb_retry_limit = 10;
重试的限制
默认情况下,TiDB 不会重试事务,因为这可能导致更新丢失并破坏 REPEATABLE READ 隔离。
原因可以从重试流程中看出:
- 分配一个新的timestamp,记为
start_ts。 - 重试包含写操作的 SQL statement。
- 执行两阶段提交。
在第 2 步,TiDB 只会重试包含写操作的 SQL statement。但在重试时,TiDB 会收到一个新的版本号作为事务的起始点。这意味着 TiDB 会用新的 start_ts 版本的数据重试 SQL statement。此时,如果事务根据其他query结果进行update,结果可能会不一致,因为违反了 REPEATABLE READ isolation。
如果你的应用可以tolerate更新丢失,并且不要求 REPEATABLE READ isolation的一致性,可以通过设置 tidb_disable_txn_auto_retry = OFF 启用该功能。
冲突检测
作为一款distributed database,TiDB 在 TiKV 层进行内存conflict检测,主要发生在 prewrite 阶段。TiDB instance 是stateless且彼此不可见,这意味着它们无法知道自己的写操作是否会在整个cluster中产生conflict。因此,conflict检测在 TiKV 层完成。
配置如下:
# 控制槽位数量。(默认 "2048000")
scheduler-concurrency = 2048000
此外,TiKV 支持监控调度器中等待 latch 的耗时。
当 Scheduler latch wait duration 较高且没有慢写入时,可以安全地判断此时存在大量写conflict。
