diff --git a/media/pessimistic-transaction-pipelining.png b/media/pessimistic-transaction-pipelining.png new file mode 100644 index 000000000000..7149c374077f Binary files /dev/null and b/media/pessimistic-transaction-pipelining.png differ diff --git a/pessimistic-transaction.md b/pessimistic-transaction.md index 410292f2a491..7992bc76d6e3 100644 --- a/pessimistic-transaction.md +++ b/pessimistic-transaction.md @@ -7,39 +7,45 @@ aliases: ['/docs-cn/dev/reference/transactions/transaction-pessimistic/'] # TiDB 悲观事务模型 -在 v3.0.8 之前,TiDB 默认使用的乐观事务模式会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。悲观事务模式可以避免这个问题,应用程序无需添加重试逻辑,就可以正常执行。 +为了使 TiDB 的使用方式更加贴近传统数据库,降低用户迁移的成本,TiDB 自 v3.0 版本开始在乐观事务模型的基础上支持了悲观事务模型。本文将介绍 TiDB 悲观事务的相关特性。 -## 悲观事务的使用方法 +> **注意:** +> +> 自 v3.0.8 开始,新创建的 TiDB 集群默认使用悲观事务模型。但如果从 v3.0.7 版本及之前创建的集群升级到 >= v3.0.8 的版本,则不会改变默认的事务模型,即**只有新创建的集群才会默认使用悲观事务模型**。 -进入悲观事务模式有以下三种方式: +## 事务模式的修改方法 -- 执行 `BEGIN PESSIMISTIC;` 语句开启的事务,会进入悲观事务模式。 -可以通过写成注释的形式 `BEGIN /*!90000 PESSIMISTIC */;` 来兼容 MySQL 语法。 +你可以使用 [`tidb_txn_mode`](/tidb-specific-system-variables.md#tidb_txn_mode) 系统变量设置事务模式。执行以下命令,即可使整个集群中所有新创建 session 执行的所有显示事务(即非 autocommit 的事务)进入悲观事务模式: -- 执行 `set @@tidb_txn_mode = 'pessimistic';`,使这个 session 执行的所有显式事务(即非 autocommit 的事务)都会进入悲观事务模式。 +{{< copyable "sql" >}} -- 执行 `set @@global.tidb_txn_mode = 'pessimistic';`,使之后整个集群所有新创建 session 执行的所有显示事务(即非 autocommit 的事务)都会进入悲观事务模式。 +```sql +set @@global.tidb_txn_mode = 'pessimistic'; +``` -在配置了 `global.tidb_txn_mode` 为 `pessimistic` 之后,默认进入悲观事务模式,但是可以用以下三种方式使事务进入乐观事务模式: +除此之外,还可以执行以下 SQL 语句显式地开启悲观事务: -- 执行 `BEGIN OPTIMISTIC;` 语句开启的事务,会进入乐观事务模式。 -可以通过写成注释的形式 `BEGIN /*!90000 OPTIMISTIC */;` 来兼容 MySQL 语法。 +{{< copyable "sql" >}} -- 执行 `set @@tidb_txn_mode = 'optimistic';` 或 `set @@tidb_txn_mode = '';`,使当前的 session 执行的事务进入乐观事务模式。 +```sql +BEGIN PESSIMISTIC; +``` -- 执行 `set @@global.tidb_txn_mode = 'optimistic;'` 或 `set @@global.tidb_txn_mode = '';`,使之后整个集群所有新创建 session 执行的事务都进入乐观事务模式。 +{{< copyable "sql" >}} -`BEGIN PESSIMISTIC;` 和 `BEGIN OPTIMISTIC;` 语句的优先级高于 `tidb_txn_mode` 系统变量。使用这两个语句开启的事务,会忽略系统变量,从而支持悲观、乐观事务混合使用。 +``` +BEGIN /*!90000 PESSIMISTIC */; +``` -如果想要禁用悲观事务特性,可以修改 TiDB 配置文件,在 `[pessimistic-txn]` 类别下添加 `enable = false`。 +`BEGIN PESSIMISTIC;` 和 `BEGIN OPTIMISTIC;` 等语句的优先级高于 `tidb_txn_mode` 系统变量。使用这两个语句开启的事务,会忽略系统变量,从而支持悲观、乐观事务混合使用。 ## 悲观事务模式的行为 悲观事务的行为和 MySQL 基本一致(不一致之处详见[和 MySQL InnoDB 的差异](#和-mysql-innodb-的差异)): -- `SELECT FOR UPDATE` 会读取已提交的最新数据,并对读取到的数据加悲观锁。 +- `SELECT FOR UPDATE` 会读取已提交的**最新**数据,并对读取到的数据加悲观锁。 -- `UPDATE`、`DELETE` 和 `INSERT` 语句都会读取已提交的最新的数据来执行,并对修改的数据加悲观锁。 +- `UPDATE`、`DELETE` 和 `INSERT` 语句都会读取已提交的**最新**的数据来执行,并对修改的数据加悲观锁。 - 当一行数据被加了悲观锁以后,其他尝试修改这一行的写事务会被阻塞,等待悲观锁的释放。 @@ -56,13 +62,14 @@ aliases: ['/docs-cn/dev/reference/transactions/transaction-pessimistic/'] - 通过设置 `innodb_lock_wait_timeout` 变量,设置等锁超时时间,等锁超时后返回兼容 MySQL 的错误码 `1205`。 - 支持 `FOR UPDATE NOWAIT` 语法,遇到锁时不会阻塞等锁,而是返回兼容 MySQL 的错误码 `3572`。 + - 如果 `Point Get` 和 `Batch Point Get` 算子没有读到数据,依然会对给定的主键或者唯一键加锁,阻塞其他事务对相同主键唯一键加锁或者进行写入操作。 ## 和 MySQL InnoDB 的差异 1. TiDB 使用 range 作为 WHERE 条件,执行 DML 和 `SELECT FOR UPDATE` 语句时不会阻塞范围内并发的 `INSERT` 语句的执行。 - InnoDB 通过实现 gap lock,支持阻塞 range 内并发的 `INSERT` 语句的执行,其主要目的是为了支持 statement based binlog,因此有些业务会通过将隔离级别降低至 READ COMMITTED 来避免 gap lock 导致的并发性能问题。TiDB 不支持 gap lock,也就不需要付出相应的并发性能的代价。 + InnoDB 通过实现 gap lock,支持阻塞 range 内并发的 `INSERT` 语句的执行,其主要目的是为了支持 statement based binlog,因此有些业务会通过将隔离级别降低至 Read Committed 来避免 gap lock 导致的并发性能问题。TiDB 不支持 gap lock,也就不需要付出相应的并发性能的代价。 2. TiDB 不支持 `SELECT LOCK IN SHARE MODE`。 @@ -74,12 +81,38 @@ aliases: ['/docs-cn/dev/reference/transactions/transaction-pessimistic/'] 4. `START TRANSACTION WITH CONSISTENT SNAPSHOT` 之后,MySQL 仍然可以读取到之后在其他事务创建的表,而 TiDB 不能。 -5. autocommit 事务不支持悲观锁 +5. autocommit 事务不支持悲观锁。 所有自动提交的语句都不会加悲观锁,该类语句在用户侧感知不到区别,因为悲观事务的本质是把整个事务的重试变成了单个 DML 的重试,autocommit 事务即使在 TiDB 关闭重试时也会自动重试,效果和悲观事务相同。 自动提交的 select for update 语句也不会等锁。 -6. 对语句中 `EMBEDDED SELECT` 读到的相关数据不会加锁。 + +6. 对语句中 `EMBEDDED SELECT` 读到的相关数据不会加锁。 + +## 隔离级别 + +TiDB 在悲观事务模式下支持了 2 种隔离级别: + +1. 默认使用与 MySQL 行为相同的[可重复读隔离级别 (Repeatable Read)](/transaction-isolation-levels.md#可重复读隔离级别-repeatable-read)。 + + > **注意:** + > + > 在这种隔离级别下,DML 操作会基于已提交的最新数据来执行,行为与 MySQL 相同,但与 TiDB 乐观事务不同,请参考[与 MySQL 可重复读隔离级别的区别](/transaction-isolation-levels.md#与-mysql-可重复读隔离级别的区别)。 + +2. 使用 [`SET TRANSACTION`](/sql-statements/sql-statement-set-transaction.md) 语句可将隔离级别设置为[读已提交隔离级别 (Read Committed)](/transaction-isolation-levels.md#读已提交隔离级别-read-committed)。 + +## Pipelined 加锁流程 + +加悲观锁需要向 TiKV 写入数据,要经过 Raft 提交并 apply 后才能返回,相比于乐观事务,不可避免的会增加部分延迟。为了降低加锁的开销,TiKV 实现了 pipelined 加锁流程:当数据满足加锁要求时,TiKV 立刻通知 TiDB 执行后面的请求,并异步写入悲观锁,从而降低大部分延迟,显著提升悲观事务的性能。但有较低概率悲观锁异步写入失败,可能会导致悲观事务提交失败。 + +![Pipelined pessimistic lock](/media/pessimistic-transaction-pipelining.png) + +该功能默认关闭,可修改 TiKV 配置启用: + +```toml +[pessimistic-txn] +pipelined = true +``` ## 常见问题 @@ -93,4 +126,4 @@ aliases: ['/docs-cn/dev/reference/transactions/transaction-pessimistic/'] 3. 悲观事务执行时间限制。 - 除了有事务执行时间不能超出 `tikv_gc_life_time` 的限制外,悲观事务的 TTL 有 10 分钟上限,所以执行时间超过 10 分钟的悲观事务有可能提交失败。 + 在 v4.0 中,GC 已不会影响到正在运行的事务,但悲观事务的执行时间仍有上限,默认为 10 分钟,可通过 TiDB 配置文件 `[performance]` 类别下的 `max-txn-ttl` 修改。