TiDB 事务与锁整理
作者: 苏州刘三枪原文来源:https://tidb.net/blog/865b670e
笔记有点乱,整理一下,以备查阅,如有错误请指出。
一、TiDB 隔离级别
TiDB 支持的隔离级别是 SI(Snapshot Isolation)。
只能读取提交后的数据,并且只能读取早于 start_ts 提交的其它事务
参数 transaction_isolation 只是为了兼容 MySQL,在 TiDB 无实际意义。
1.1 写不阻塞读
1.2 线性一致
示例,原始数据:no=001
txn1 start_ts 为 8 点,更新 no=002,未提交
txn2 start_ts 为 9 点,读取不会阻塞,只能读取到 no=001
txn1 在 9 点 10 分 提交
txn2 在 9 点 20 分 继续读取,读取不会阻塞,也只能读取到 no=001
小结:
1、TiDB 不会产生幻读
2、只能读取到 Commit_TS < Start_TS 的数据
1.3 MVCC 读写流程
原始数据:1,Hilen
事务 A 在 TSO=100 时,写入数据:1,Alice
然后写入锁信息到 Lock CF,直到开始进行 2PC 中的 Commit 写入数据到 Write CF
事务 B 在 TSO=120 时,开始读取 id = 1 的数据
读取流程
(1) 首先在 Write CF 查找,找到 版本信息为 110 的数据
(2) 检查 Lock CF,根据 110 找到锁信息,这里有锁,事务还未提交完成
(3) 继续查找上一个版本的数据:1,Hilen
修改流程
(1)
二、TiDB 事务
自 TiDB 3.0.8 起默认为悲观事务模型,参数 tidb_txn_mode=‘pessimistic’。
使用悲观事务模型时:
自动提交事务首先尝试使用开销更小的乐观事务模式进行提交
如果发生了写冲突,重试时才会使用悲观事务提交
如果想要在首次提交时就使用悲观事务,需要满足下面 2 个条件:
tidb_txn_mode=‘pessimistic’
显示开启事务 [ BEGIN | START TRANSACTION ] / COMMIT
或者关闭自动提交 autocommit=0 (生产应该不会有这个骚操作),默认使用悲观事务模式。
使用 BEGIN PESSIMISTIC、BEGIN OPTIMISTIC 的优先级高于 tidb_txn_mode 系统变量。使用这两个语句开启的事务,会忽略系统变量,从而支持悲观、乐观事务混合使用。
2.1 事务自动重试 (显示事务不会重试)
自动提交事务遇到写冲突会报错,然后进行自动重试,直到【悲观事务参数】见下文,达到限制将会返回给客户端错误。可以通过 grep ‘prewrite encounters lock|Write conflict’ tidb.log 查看锁冲突时的报错信息
- prewrite encounters lock
- pessimistic write conflict, retry statement
2.2 悲观事务参数
pessimistic-txn.max-retry-count
默认值:256,悲观事务中单个语句最大重试次数。超过则会报错:pessimistic lock retry limit reached;
可以通过 grep -i Exec_retry_count tidb_slow_query.log 查看 SQL 的重试次数
innodb_lock_wait_timeout
默认值:50 秒,在悲观锁模式下,事务最大等锁时间。超过则会报错:Lock wait timeout exceeded;try restarting transaction;
2.3 乐观事务参数
tidb_disable_txn_auto_retry
默认为 false,表示不禁用显式的乐观事务自动重试。
tidb_retry_limit
默认为 10,表示乐观事务的自动重试次数。
2.4 事务其他重要参数
storage.scheduler-concurrency: 2048000
scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。每个 key hash 到不同的槽。
performance.committer-concurrency
默认为 16,在单个事务的提交阶段,用于执行提交操作相关请求的 goroutine 数量;
performance.max-txn-ttl
默认为 10 分钟,悲观事务的执行时间的上限,它的含义是从悲观事务第一次加锁,或者乐观事务的第一个 prewrite 开始。超过则报错:TTL manager has timed out;
performance.stmt-count-limit
默认为 5000,单个事务最大语句数。
performance.txn-total-size-limit
默认为 100MB,最大为 10G。
enable-batch-dml 默认 false,官方建议禁用此参数。
max-txn-time-use 默认 gc_lifetime-10,4.0 已经移除此参数。升级版本到 4.0 时需要删除此参数。
2.5 TiDB 事务的限制
单个事务包含的 SQL 语句不超过 5000 条(可修改)
每个键值对不超过 6 MB (4.0.10 开始支持:raft-entry-max-size、txn-entry-size-limit)
- 键值对的总数不超过 300000 (代码写死,无法修改)
键值对的总大小不超过 100 MB (可修改,不超过 10GB。DELETE 语句不受此限制)
注意:
开启大事务建议最大不超过 2G,事务大小只受 txn-total-size-limit 控制,开启后 30W 键值对的总数失效。
事务对内存的占用可能会有 3-4 倍的放大,10GB 大的事务可能会占用 30-40GB 的内存。如果需要执行特别大的事务,需要提前做好内存的规划,避免对业务产生影响。
三、乐观事务模型
TiDB 中事务使用两阶段提交协议,和 MySQL 中的 2PC 不一样,参考图片:
再来一张简图:
3.1 事务执行完整流程
客户端开始一个事务
TiDB 从 PD 获取 start_ts,start_ts 同时也作为该事务获取的数据库快照版本
客户端发起读请求
TiDB 从 PD 获取数据路由信息,即数据具体存在哪个 TiKV 节点上
TiDB 从 TiKV 获取 start_ts 版本下对应的数据
客户端发起写请求 ( 如果只是读取数据,这里就应该为 TiDB 将数据返回给客户端 ),TiDB 校验写入数据是否符合约束(如数据类型是否正确、是否符合非空约束等)。校验通过的数据将存放在 TiDB 中该事务的私有内存里。
客户端发起 commit
TiDB 开始两阶段提交,见 3.2 节
TiDB 向客户端返回事务提交成功
TiDB 异步清理本次事务遗留的锁信息
3.2 两阶段提交 2PC
RocksDB Column Family 简称:
D 列:rocksdb.defaultcf
L 列:rocksdb.lockcf
W 列:rocksdb.writecf
事务提交前,会在 TiDB 缓存所有数据。这边有遇到过同时并发写入上百 MB 数据,结果导致 TiDB OOM。
1、TiDB 从当前要写入的行中选择一个 Key 作为当前事务的 Primary Key,剩于的 Keys 为 Secondary
2、TiDB 从 PD 获取所有数据的写入路由信息,并将所有的 Key 按照路由进行分类
3、TiDB 发起 prewrite 请求,将 Primary Key 与数据写入到 TiKV,并进行加锁【加锁前会检查是否有写入冲突】,
**** 加锁成功后执行下面操作:
(1) 锁信息写入 L 列,示例:<1,(W,pk,1,100 ... )>
(2) 行数据写入 D 列,示例:put<1\_100,'相亲相爱一家人'>
4、然后 Secondary Key 并发地向所有涉及的 TiKV 发起 prewrite 请求,流程同 Primary Key 类似
**** 区别是锁信息指向了 Primary Key :
(1) 锁信息写入 L 列,示例:<2,(W,**@1**,2,100 ... )>
5、TiDB 收到所有 prewrite 都成功
6、TiDB 向 PD 获取 commit_ts
7、TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交的 Commit
(1) 写入 W 列,示例:put<1\_110,100>
(2) 写入 L 列,表示删除锁信息,示例:<1,(D,pk,1,100 ... )>
(3) 最后清理锁信息
8、Primary Commit 提交成功后,Secondary 可以进行异步提交
9、TiDB 收到两阶段提交成功
【加锁前会检查是否有写入冲突】
检查 L 列,是否已经有别的客户端已经上锁 (Locking)
检查 W 列,在本次事务开始时间之后,是否有更新 [startTs, +Inf) 的写操作已经提交 (Conflict)
Prewrite 出现冲突,当前事务回滚。
Primary Commit 出现冲突,全事务回滚。
3.2 乐观事务优缺点
优点:事务在二阶段提交的 Prewrite 时才会检测冲突。在事务提交的过程中锁检测的代价是比较大的,所以乐观事务在一些场景有较好的写入提升。比如基于 id 自增主键的写入情景,或者有唯一索引但是很少或者不会出现多个并发同时对同一个行的 DML 操作的情景。
缺点:事务冲突不可避免,乐观模式采用了内部重试功能。
重试的好处:写冲突的情况避免直接报错给 client。
重试的缺点:每次重试时间间隔会逐渐变长,写冲突高的情况下,一条 SQL 可能需要较长时间才能写入成功,另外 TiDB 默认不进行事务重试,因为重试事务可能会导致更新丢失,从而破坏可重复读的隔离级别。
四、悲观事务模型
上图:
TiDB 在乐观事务模型的基础上支持了悲观事务模型,将上锁的时机提前到进行 DML 时。TiDB 的悲观锁实现的原理确实如此,在一个事务执行 DML (UPDATE/DELETE) 的过程中,TiDB 不仅会将需要修改的行在本地缓存,同时还会对这些行直接上悲观锁,这里的悲观锁的格式和乐观事务中的锁几乎一致,但是锁的内容是空的,只是一个占位符,待到 Commit 的时候,直接将这些悲观锁改写成标准的 Percolator 模型的锁,后续流程跟乐观模型一样。
4.1 悲观事务模式的行为
悲观事务的行为和 MySQL 基本一致:
UPDATE
、DELETE
或INSERT
语句都会读取已提交的最新数据来执行,并对所修改的行加悲观锁。SELECT FOR UPDATE
语句会对已提交的最新的数据而非所修改的行加上悲观锁。悲观锁会在事务提交或回滚时释放。其他尝试修改这一行的写事务会被阻塞,等待悲观锁的释放。其他尝试 * 读取 * 这一行的事务不会被阻塞,因为 TiDB 采用多版本并发控制机制 (MVCC)。
如果多个事务尝试获取各自的锁,会出现死锁,并被检测器自动检测到。其中一个事务会被随机终止掉并返回兼容 MySQL 的错误码
1213
。如果多个事务同时等待同一个锁释放,会大致按照事务
start ts
顺序获取锁。乐观事务和悲观事务可以共存,事务可以任意指定使用乐观模式或悲观模式来执行。
支持
FOR UPDATE NOWAIT
语法,遇到锁时不会阻塞等锁,而是返回兼容 MySQL 的错误码3572
。如果
Point Get
和Batch Point Get
算子没有读到数据,依然会对给定的主键或者唯一键加锁,阻塞其他事务对相同主键唯一键加锁或者进行写入操作。
4.2 和 MySQL InnoDB 的差异
1、有些 WHERE
子句中使用了 range,TiDB 在执行这类 DML 语句和 SELECT FOR UPDATE
语句时,不会阻塞 range 内并发的 DML 语句的执行。
举例:
2、TiDB 不支持 SELECT LOCK IN SHARE MODE
。
3、DDL 可能会导致悲观事务提交失败。
MySQL 在执行 DDL 语句时,会被正在执行的事务阻塞住,而在 TiDB 中 DDL 操作会成功,造成悲观事务提交失败:ERROR 1105 (HY000): Information schema is changed. [try again later]
。TiDB 事务执行过程中并发执行 TRUNCATE TABLE
语句,可能会导致事务报错 table doesn't exist
。
4、START TRANSACTION WITH CONSISTENT SNAPSHOT
之后,MySQL 仍然可以读取到之后在其他事务创建的表,而 TiDB 不能。
5、Autocommit 事务优先采用乐观事务提交。
6、对语句中 EMBEDDED SELECT
读到的相关数据不会加锁。
7、垃圾回收 (GC) 不会影响到正在执行的事务
五、Pipelined 特性
加悲观锁需要向 TiKV 写入数据,要经过 Raft 提交并 apply 后才能返回,相比于乐观事务,不可避免的会增加部分延迟。为了降低加锁的开销,TiKV 实现了 pipelined 加锁流程:当数据满足加锁要求时,TiKV 立刻通知 TiDB 执行后面的请求,并异步写入悲观锁,从而降低大部分延迟,显著提升悲观事务的性能。但当 TiKV 出现网络隔离或者节点宕机时,悲观锁异步写入有可能失败,从而产生以下影响:
无法阻塞修改相同数据的其他事务。如果业务逻辑依赖加锁或等锁机制,业务逻辑的正确性将受到影响。
有较低概率导致事务提交失败,但不会影响事务正确性。
如果业务逻辑依赖加锁或等锁机制,或者即使在集群异常情况下也要尽可能保证事务提交的成功率,应关闭 pipelined 加锁功能。
该功能默认关闭,若集群是 v4.0.9 及以上版本,可通过在线修改 TiKV 配置功能动态开启该功能:
六、监控指标
6.1 锁冲突
TiDB -> KV Errors
txnlock 表示写写冲突
txnLockFast 表示读写冲突
expired、not_expired、wait_expired 表示对应的 lock 状态
七、QA
7.1 TxnLockNotFound
TxnLockNotFound 错误是由于事务提交的慢了,超过了 TTL 的时间。当要提交时,发现被其他事务给 Rollback 掉了。在开启 TiDB 自动重试事务的情况下,会自动在后台进行事务重试(注意显示和隐式事务的差别)。
pingcap/tidb/blob/master/store/tikv/lock_resolver.go#L124
defaultLockTTL 默认为 3s 代码写死不能修改,这里的 TTL 并不是 performance.max-txn-ttl。
区别如下:
performance.max-txn-ttl:从客户端发起一个事务开始执行的最长时间,超过则报超时错误。
defaultLockTT:指的是 2PC 中的 Primary Commit 执行超过 3s 还未提交完成,则有可能被其他事务回滚掉。嫌弃你提交的太慢了,长时间占着锁不释放,把你的锁清理掉我先加锁执行。然后你自动重试吧。
继续完善中。
References
[2] TiDB 新特性漫谈:悲观事务
[3] 线性一致性和 Raft
[6] Large-scale Incremental Processing Using Distributed Transactions and Notifications
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/453774be9105d73e8a3395820】。文章转载请联系作者。
评论