TinyKV Project 4
参考资料
Project 4 通过建立一个事务系统实现多版本并发控制 MVCC。在编码之前,需要对事务的相关概念提前了解(事务的属性,事务隔离级别等)。
文档可以参考以下翻译(细节部分需要看英文文档):
实验指导书(翻译)Project 4: Transactions_从实验六中的people-CSDN博客
TinyKV的事务设计遵循 Percolator 模型,它本质上是一个两阶段提交协议(2PC)。如果看完文档还是云里雾里,可以通过这篇文章加深对 Percolator 的理解:
Percolator模型及其在TiKV中的实现-腾讯云开发者社区-腾讯云 (tencent.com)
Part A
Part A主要实现 MVCC 。基于lock、write、default三个列族,封装MvccTxn
的 API,代码主要在transaction.go
中,里面提供了一些用于对key进行编解码的辅助函数。
|
|
事务所有的写入操作都存入writes当中,便于被一次性写到底层数据库,保障原子性。因此PutWrite
、PutLock
、DeleteLock
、PutValue
和DeleteValue
都是将 key、cf、value(delete不包含)append到writes
中。需要注意的是不同cf对应的key的构成:
- lock:CfLock + Key
- write:CfWrite + Key + CommitTs
- default:CfDefault + Key + StartTs
val的构成也不同:
|
|
GetValue
查询当前事务下,传入 key 对应的 Value。
- 通过
iter.Seek(EncodeKey(key, txn.StartTS))
查找遍历 Write,找到满足 commitTs <= ts 的最新 write; - 判断该 write 的 userkey 与当前 key相同,如果不是,说明不存在,直接返回;
- 判断该 write 的
Kind
是不是WriteKindPut
,如果不是,说明不存在,直接返回; - 从 Default 中通过 EncodeKey(key, write.StartTS) 获取目标 Value;
CurrentWrite
查询当前事务下,传入 key 的最新 write。
- 通过
iter.Seek(EncodeKey(key, ^uint64(0)))
查询该 key 的最新 write; - 如果
write.StartTS > txn.StartTS
,继续遍历,直到找到write.StartTS == txn.StartTS
的 write; - 返回这个 write 和 它的 commitTs;
MostRecentWrite
查询传入 key 的最新 write,不用考虑当前事务的开始时间戳。
Part B
Part B主要利用 A 中封装的 Mvcc API 实现 Percolator 模型的事务性 API,包括KvGet
、KvPrewrite
和KvCommit
,分别对应读写过程。
KvGet
对应 Percolator 的读过程:
获取一个时间戳ts(
req.GetVersion
),并创建一个MvccTxn
;查询当前我们要读取的 key 上是否存在一个时间戳在[0, ts]范围内的锁。
- 如果存在一个时间戳在[0, ts]范围的锁,那么意味着当前的数据被一个比当前事务更早启动的事务锁定了,但是当前这个事务还没有提交。此时直接返回
Locked
错误。 - 如果没有锁,或者锁的时间戳大于ts,那么读请求可以被满足。
- 如果存在一个时间戳在[0, ts]范围的锁,那么意味着当前的数据被一个比当前事务更早启动的事务锁定了,但是当前这个事务还没有提交。此时直接返回
从write列族中查询在[0, ts]范围内的最大 commit_ts 的记录,然后依此获取到对应的start_ts。
根据上一步获取的start_ts,从data列获取对应的记录(3,4两步已经被封装在
GetValue
当中);如果发生
RegionError
或者未找到要返回对应的错误。
KvPrewrite 和 KvCommit
对应 2PC 的 Prewrite 和 Commit两个阶段。
在Prewrite阶段:
利用
req.GetStartVersion()
创建一个 Mvcctxn
;遍历
req.Mutations
,对于其中的每一个key
:- 利用
txn.MostRecentWrite
检查是否一个有一个比startTs
更大的新write,如果存在,则append一个WriteConflict
错误到keyErrors
; - 利用
txn.GetLock
检查是否已经存在一个lock,如果存在,则append一个Locked
错误到keyErrors
; - 根据
WriteKind
调用txn.PutValue
或txn.DeleteValue
; - 利用
txn.PutLock
写入该key的lock。
- 利用
如果
len(keyErrors) > 0
,作为响应返回;最后利用
server.storage.Write(req.Context, txn.Writes())
一次性写到底层数据库
在Commit阶段:
利用
req.GetStartVersion()
创建一个 Mvcctxn
;
server.Latches.WaitForLatches(req.Keys)
锁定 Commit 请求中涉及的所有key,避免局部竞争条件;遍历请求中的所有
key
:- 利用
txn.CurrentWrite(key)
检查是否有重复的提交,如果是重复提交则返回; - 利用
txn.GetLock(key)
检查是否 lock 依然存在,如果lock为空或者lock的时间戳与当前请求的StartVersion
不相等,说明该事务已经被其他事务清理,返回一个Retryable
错误; -
txn.PutWrite
,txn.DeleteLock
- 利用
server.storage.Write(req.Context, txn.Writes())
一次性写到底层数据库,完成提交。
Part C
KvScan
扫描操作需要利用mvcc.NewScanner(req.StartKey, txn)
创建一个scanner
来进行迭代,对于迭代过程中每个key的处理,都是与KvGet
中类似的,需要检查一下lock的情况;如果某个key出现错误,不能中止迭代,将错误信息append到最终的结果当中即可。
KvCheckTxnStatus
用于 Client failure 后,想继续执行时,先检查 Primary Key 的状态,以此决定是回滚还是继续推进 commit。
- 通过
txn.CurrentWrite(req.PrimaryKey)
获取 primary key 的write
,如果write
且不是WriteKindRollback
,则说明已经被commit了,直接返回 commitTs;如果是WriteKindRollback
说明已经被回滚,因此无需操作,返回Action_NoAction
即可。 - 通过
txn.GetLock(req.PrimaryKey)
获取 primary key 的lock
;如果lock == nil
,打上 rollback 标记(WriteKindRollback
),返回Action_LockNotExistRollback
; - 如果
lock
存在,并且超时了。那么删除这个lock
,并且删除对应的Value
,同时打上rollback标记,然后返回Action_TTLExpireRollback
; - 如果以上条件均为发生,说明
write
不存在,且lock
存在但并没有超时,直接返回,事务继续执行。
根据文档,检查lock超时要用到mvcc.PhysicalTime
,方法如下
|
|
KvBatchRollback
回滚操作与Commit逻辑相反,执行流程是相似的:
server.Latches.WaitForLatches(req.Keys)
锁定本次回滚操作涉及的所有key遍历所有key
-
txn.CurrentWrite(key)
获取当前事务下的write
,如果不为空,若write.Kind == mvcc.WriteKindRollback
,说明该key
已经回滚,continue,否则说明事务已提交,直接返回一个Abort
响应; - 利用
txn.GetLock(k)
获取lock
,若lock
为空或者不是本事务的,则直接打上 rollback 标记(WriteKindRollback
)并continue;若本事务还持有lock
,则要依次txn.DeleteLock
、txn.DeleteValue
、打上 rollback 标记。
-
server.storage.Write(req.Context, txn.Writes())
一次性写到底层数据库,完成rollback。
KvResolveLock
这个方法主要用于解决锁冲突,当客户端已经通过KvCheckTxnStatus()
检查了 primary key 的状态,要么全部回滚,要么全部提交,具体取决于 ResolveLockRequest
的CommitVersion
。
- 首先遍历
CfLock
,找到当前事务下的所有现存lock,将对应的key加入集合keys
,若len(keys) == 0
,直接返回; - 若
req.CommitVersion == 0
,调用KvBatchRollback
全部回滚;否则,调用KvCommit
全部提交