在 3.0.8 之前,TiDB 的默认事务模式是乐观事务, TiDB 乐观事务存在以下优点:
缺点如下:
基于以上缺点,我们有了后面的建议。
事务的冲突,主要分两种:
在 TiDB 的乐观锁机制中,因为是在客户端对事务 commit 时,才会触发两阶段提交,检测是否存在写写冲突。所以,在乐观锁中,存在写写冲突时,很容易在事务提交时暴露,因而更容易被用户感知。
频繁乐观锁事务冲突会对应用的整体性能造成很大影响
业务上并发修改的场景(如账本, 余额, 秒杀等), 需要改造业务, 减少乐观锁事务冲突, 常见方案有
TiDB 两阶段提交的网络开销相对较大,因此建议将多个单行事务(100~500 行)打包成一个多行事务发送
当事务过大时,会有以下问题
因此目前 TiDB 对大事务有如下限制
检测数据是否存在写写冲突是一个很重的操作,这个操作在 prewrite 时 TiKV 中具体执行。为了优化这一块性能,TiDB 集群会在内存里面进行一次冲突预检测。
主要在两个模块进行:
其中 TiDB 层的冲突检测通过下面参数控制:
txn-local-latches 事务内存锁相关配置,当本地事务冲突比较多时建议开启。 enable 开启 默认值:false
capacity Hash 对应的 slot 数,会自动向上调整为 2 的指数倍。每个 slot 占 32 Bytes 内存。当写入数据的范围比较广时(如导数据),设置过小会导致变慢,性能下降。 默认值:1024000
这里 capacity 的配置会影响到冲突判断的正确性。在实现冲突检测时,真正存下来的是每个 key 的 hash 值,有 hash 算法就有误判的概率,这里我们通过 capacity 来控制 hash 取模的值:
如果业务场景能够预判断写入不存在冲突,建议关闭 TiDB 层的冲突检测。
TiKV 内存中的冲突检测也有一套类似的机制。不同的是,TiKV 的检测是必须进行的,只提供了一个 hash 取模值的配置项:
scheduler-concurrency scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。每个 key hash 到不同的槽。 默认值:2048000
此外,TiKV 提供了监控查看具体消耗在 latch 等待的时间:
如果发现这个 wait duration 特别高,说明耗在等待锁的请求上比较久,如果不存在底层写入慢问题的话,基本上可以判断这段时间内冲突比较多。
由于乐观锁是在 commit 阶段检测事务冲突,在冲突比较大的时候,Commit 很容易出现失败,而悲观锁模式数据库如 MySQL 的冲突检测在 SQL 执行过程中执行,所以 commit 时很难出现异常。为了解决这种行为不一致的问题, TiDB 提供了重试机制,由以下两个参数控制:
重试的步骤如下:
打开乐观锁重试时(tidb_disable_txn_auto_retry = off),如果事务写入的执行条件依赖于这个事务中先前读到的结果,并发执行就会有 lost update 问题。此时不要打开 TiDB 乐观锁的重试机制。需要注意的是,如果是一些内部原因引起的重试不需要新的 tso, 例如网络抖动,平衡 region 等原因引起的重试,TiDB 会自动的,安全的重试,即使tidb_disable_txn_auto_retry = off
打开了重试后,我们来看下面的例子:
时间 | Session A | Session B |
---|---|---|
t1 | MySQL [test]> begin; Query OK, 0 rows affected (0.00 sec) |
|
t2 | MySQL [test]> begin; Query OK, 0 rows affected (0.00 sec) |
|
t3 | MySQL [test]> update tidb set status=0 where id=1; Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
t4 | MySQL [test]> select * from tidb where id=1; +——+——+——–+ | id | name | status | +——+——+——–+ | 1 | tikv | 1 | +——+——+——–+ 1 row in set (0.01 sec) |
|
t5 | commit; Query OK, 0 rows affected (0.01 sec) |
|
t6 | update tidb set name=’pd’ where id =1 and status=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
t7 | MySQL [test]> select * from tidb where id=1; +——+——+——–+ | id | name | status | +——+——+——–+ | 1 | pd |1 | +——+——+——–+ 1 row in set (0.01 sec) |
|
t8 | MySQL [test]> commit; Query OK, 0 rows affected (0.01 sec) |
|
t9 | MySQL [test]> select * from tidb where id=1; +——+——+——–+ | id | name | status | +——+——+——–+ | 1 | tikv | 0 | +——+——+——–+ 1 row in set (0.00 sec) |
我们来详细分析以下这个 case:
这里我们可以看到,对于重试事务,如果本身事务中更新语句需要依赖查询结果时,因为重试时会重新取版本号作为 start_ts,因而无法保证事务原本的 ReadRepeatable 隔离型,结果与预测可能出现不一致。
综上所述,如果存在依赖查询结果来更新 SQL 语句的事务,建议不要打开TiDB 乐观锁的重试机制。
在 TiDB 乐观事务模型下有一些缺点,需要应用,架构层进行改造。同时为了克服这些缺点,满足更加严苛的场景,TiDB 实现了悲观事务,可以参考悲观事务章节。
#