tikv事务优化 async commit阅读笔记
阅读了tikv的sig-transaction中关于async commit的优化的方案,做一下简单的笔记。
思路
percolator是一种2PC协议,一次正常的事务提交至少需要两次从tidb到tikv的round trip, async-commit则尽量希望能尽量减少一次round trip。其基本的想法为:在primary key的记录中记录下所有的secondary key的值,prewrite完成后就认为事务已经提交。
能这么做的主要原因在于primary key中记录了所有secondary key,因此可以通过primary key找到所有secondary key,即使发生了crash也可以通过这些信息判断事务是否完成提交,只要primary key和所有secondary key都已经完成写入,则这个事务就已经提交,事务的commit point为最后一个key完成写入的那一瞬间。
commit TS选择
按这种思路最关键的一个问题在于如何选择commitTS,percolator事务需要startTS和commitTS来判断是否相可见,一个事务只能读到commit TS小于它的start TS的记录,如果选择的commitTS不恰当,则会影响系统外部线性一致。需要杜绝以下两个情况:
- 这个事务提交后,随后开启的事务读取不到它写入的数据。(因此该事务的commit TS需要比后续事务的start TS小,以便后面的事务能够读取到它的数据)
- 必须比没有读到锁的事务的startTS大。(如果这个事务的commitTS小于没有读取到锁的事务的startTS,那么按理读取事务应该能读取到数据,但是由于还没读取到锁,因此读取事务直接去读最近的可见的版本了)
对于第一种情况,可以否定一种方案,那就是prewrite完成后,再异步的去PD获取一个TS进行commit,因为prewrite后就返回了client,如果这时候client新开了一个事务去读取刚提交的数据,那么它的TS可能是小于异步获取的commitTS的,可能会读取不到已经提交的数据。对于第二种情况,如果一个事务在锁写入前读取了某行数据,那么这个事务的startTS必须小于要提交的commitTS,否则会读取不到该读取到的数据。
由于async commit是prewrite的最后一个key写入完成就需要能确定commitTS,一个思路是:可以由tikv维护一个maxTS,表示当前tikv所见到的最大的TS,在每个key里prewrite写入的时候带上这个maxTS。prewrite完成后,选择返回的所有写入成功的key所带的TS的最大值+1作为commitTS。
对于第一个问题,由于新的事务需要去PD获取一个startTS,而最终选定的commitTS是从各个tikv节点见到的TS中选取的,由于PD分配的TS是单调递增的,所以新事务的startTS一定大于之前async commit的事务。这个问题能解决。
对于第二个问题,看上去好像可以,因为是使用的当前见到的最大的TS+1作为commitTS,按理应该不会存在一个再写入前已经去读取过数据而startTS又大于commitTS的事务,但是关键在于prewrite写入是需要时间的,例如,在某一时刻选择了TS1作为commitTS,但是写入还没有完成,此时有一个更大的startTS为TS2的事务来进行读取,此时prewrite还没写入成功,依然读取不到锁,无法解决上面的问题二。
解决这类问题的本质是:推迟可能发生错误的操作直到确保没有错误。解决办法有:
- region 记录 min commit ts,即当前正在进行的 parallel commit 事务的最小的 commit ts。若读请求的 start ts 大于 min commit ts 就阻塞住直到 min commit ts 大于 start ts。也就是有可能发生错误的读请求被阻塞到确保没错误时再执行。
- 方法 1 粒度太大,为 region 级别,可以 region 内再划分 range,最细的粒度为 key 级别,也就是下面的方法:a. lock 先写到内存中,然后获取 max start ts 再写到 raftstore。读的时候先读内存中的 lock。写入到 raftstore 成功后清理掉 mem 中的锁,leader 切换时清理掉 region 对应的锁。b. 用 rocksdb 当内存锁的存储介质,先写到 rocksdb,再写到 raftstore。实现也简单,效果和 a一样,要注意清理。