TiDB 事务源码阅读
作者: crazycs520-PingCAP 原文来源:https://tidb.net/blog/439b216b
资料
乐观事务
TiDB 的乐观事务在提交前,所有的写操作都是 buffer 在内存的,最后提交时才会写到底层 TiKV 。
TiDB 有 2 层 事务抽象,一层是 session.TxnState,另一层是 kv.Transaction :
Write
TiDB 在执行 insert/update/delete 等 DML 时,会先把写的数据存在 TxnState.buf 里面,当 DML 语句执行成功后,会调用 StmtCommit 方法把写的数据刷到 kv.Transaction里面,如果执行失败,就调 StmtRollback 将 TxnState里面的 buf 清空。其实在写到 TxnState.buf 之前,可能还会先写到一个 buffer 里面,可以看 tableCommon.AddRecord 函数,这个函数的作用是写一行新的数据到某个表里面:
为什么要将索引先写到一个临时的 buffer 里面?举个例子:
表 t 有 2 个索引,a 列上有一个普通索引,b 列上有一个唯一索引。在执行第二个 insert 写入时,会因为 b 列上的唯一索引冲突而失败,下面详细分析第二次 insert 的写入过程 :
如果索引不是写在临时 buffer 里面,而是直接写在 TxnState.Buf 里面,在报错返回后,会导致 TxnState.Buf 里面多了一个索引 a 的数据,但是对应的行数据又不存在,这就导致了索引数据不一致。
Read
RawKV Read
在事务里面读数据,需要先读 buffer 里面的数据,buffer 里面没有,再去 TiKV 读数据。可以看 TxnState.Get 函数,它会先尝试从 TxnState.buf读,没有再去 kv.Transaction的 buffer 读,再没有就去 TiKV 读数据,见tikvSnapshot.Get 函数。
Cop Read
对于 TableReaderExecutor,IndexReaderExecutor,IndexLookUpExecutor 这 3 种 Cop 读请求类型的算子,在事务读里面,会包一层 UnionScanExec算子。
假设原来是 TableReaderExecutor算子 ,被UnionScanExec包了一层后, UnionScanExec 内部还会生成另外一个memTableReader 算子,用来读取事务 Buffer 里面的数据,原来的 TableReaderExecutor 用来从 TiKV 读数据,UnionScanExec 算子会将这两个内部算子读出来的数据做合并后输出,具体见 UnionScanExec.getOneRow 函数,唯一注意的是,如果从 TiKV 读回来的数据如果发现在事务 buffer 里面也存在的话,就会丢弃从 TiKV 读上来的数据,而用事务 buffer 里面的数据。
Commit
事务提交是直接调的 kv.Transaction 的 Commit 方法,其具体实现是 tikvTxn.Commit。TiDB 的事务总体来说就是一个经过优化的二阶段提交的实现。所以会先初始化一个 twoPhaseCommitter ,然后用 initKeysAndMutations 将 buffer 里面的数据转成 pb.Mutation map,并同时做一些事务限制的检查,然后根据 key/value 的 size 大小计算 lock TTL 时间(txnLockTTL)。一共有下面几种类型的 mutation:
twoPhaseCommitter.execute 是 2PC 的实现,首先会 prewriteKeys,用 doActionOnKeys 这个函数来对 keys 做 prewrite、commit、rollback,cleanup 等操作。其具体实现步骤如下:
如果 prewrite 的具体实现函数是 prewriteSingleBatch,如果 prewrite 失败,就会终止 commit 然后异步清理(cleanupKeys)已经 prewrite 了的 key.
prewrite 成功后,会用 getTimestampWithRetry 去 PD 拿一个 commitTS 的时间戳。
然后会检查这个事务操作的相关 table 是否在事务期间有相关的 schema 变更,以及事务时间是否超过了 max-txn-time-use 的限制。通过检查后,会进行 commitKeys,commit 和 prewrite 有一个不同的是,commit 只要将 primary key 提交成功后,其他的 second keys 可以走异步commit。 commit的具体实现函数是 commitSingleBatch。
ResolveLocks
当在事务中读数据或者 prewrite keys 时,如果 key 上已经有 Lock 了,这时就需要进行 ResolveLock 。为什么会出现这种情况?有以下几种情况:
事务 txn_1 在完成
prewritekey_1,key_2 后就异常退出了,那么此时事务 txn_2 再去读 key_1, key_2 时,就会发现有 txn_1 在prewrite时写的 Lock。事务 txn_1 在
prewritekey_1 完成, 但在 key_2 因为冲突而失败时,txn_1 会终止并异步清理 key_1 上的锁,如果异步清理锁还没完成,此时 txn_2 去读 key_1 ,也会遇到 Lock事务 txn_1 在
commitprimary key 成功后,是用异步commitsecond keys,在异步commit还没完成时,txn_2 去读 second keys 时也会遇到 Lock。
那么如何 ResolveLocks 呢?prewrite keys 时会同时带上一个 LockTTL, ResolveLock的流程首先是检查所有 Lock 的 TTL,记下最久的 expire Time ,并发现如果有 keys 上的 locks 的 ttl 已经过期后,就会发起对这些过期 keys 的 Locks 进行 resolveLock,如果还有 keys 的 locks 没有 resolve,就根据最久的 expire time 进行 back off 后重试。
resolve 已经过期的 Locks 的流程如下 (resolveLock 函数):
用
getTxnStatusFromLock查询 Lock key 的状态如果 lock 以及过期,并根据 Lock 的状态发送
ResolveLockRequest类型的请到 locked key 所在的 region, 根据 lock 的状态执行 commit or rollback.
悲观事务
悲观事务会将需要修改的 key 在执行完 DML 后就上锁,具体见 handlePessimisticDML 函数。 对于 select for update, 会单独用
SelectLockExec 算子在获取相应数据后,就会对相应行的 handle key 上锁,见 doLockKeys 函数。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/44b586eae341e54aae729ffa4】。文章转载请联系作者。










评论