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 在完成
prewrite
key_1,key_2 后就异常退出了,那么此时事务 txn_2 再去读 key_1, key_2 时,就会发现有 txn_1 在prewrite
时写的 Lock。事务 txn_1 在
prewrite
key_1 完成, 但在 key_2 因为冲突而失败时,txn_1 会终止并异步清理 key_1 上的锁,如果异步清理锁还没完成,此时 txn_2 去读 key_1 ,也会遇到 Lock事务 txn_1 在
commit
primary key 成功后,是用异步commit
second 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】。文章转载请联系作者。
评论