什么是MVCC
MVCC 机制正是基于多版本技术实现的一种乐观锁机制,它乐观地认为数据不会发生冲突,但是当事务提交时,具备检测数据是否冲突的能力。
在 MVCC 数据库中,你更新一个 key-value数据的时候,它并不会直接覆盖原数据,而是新增一个版本来存储新的数据,每个数据都有一个版本号。
当你指定版本号读取数据时,它实际上访问的是版本号生成那个时间点的快照数据。当你删除数据的时候,它实际也是新增一条带删除标识的数据记录。
ETCD的实现
首先etcd在内存中使用b-tree维护了key与版本号revision的关系树treeIndex,再以revision作为boltdb的key存放value,boltdb内部同样使用b-tree作为查找结构。
get/put操作key首先在treeindex中查找/构建对应的版本revision,然后用revision作为key到boltdb中读写value。
treeIndex中每一个叶子节点为一个keyIndex结构。
type keyIndex struct {
key []byte //用户的key名称,比如我们案例中的"hello"
modified revision //最后一次修改key时的etcd版本号,比如我们案例中的刚写入hello为world1时的,版本号为2
generations []generation //generations 表示一个 key 从创建到删除的过程,每代对应 key 的一个生命周期的开始与结束,每代中包含对key的多次修改的版本号列表
}
type generation struct {
ver int64 //表示此key的修改次数
created revision //表示generation结构创建时的版本号
revs []revision //每次修改key时的revision追加到此数组
}
type revision struct {
main int64 // 一个全局递增的主版本号,随put/txn/delete事务递增,一个事务内的key main版本号是一致的
sub int64 // 一个事务内的子版本号,从0开始随事务内put/delete操作递增
}
boltdb中存储的Value为mvccpb.KeyValue结构。
type KeyValue struct {
Key []byte
CreateRevision int64 // 表示此 key 创建时的版本号
ModRevision int64 // 表示 key 最后一次修改时的版本号
Version int64 // 表示此 key 的修改次数
Value []byte
Lease int64
}
ETCD MVCC读写
更新key
初始化一个新集群,全局版本号默认为 1。
执行下面的 txn 事务,它包含两次 put、一次 get 操作,那么按照我们上面介绍的原理,全局版本号随读写事务自增,因此是 main 为 2,sub 随事务内的 put/delete 操作递增,因此 key hello 的 revison 为{2,0},key world 的 revision 为{2,1}。
$ etcdctl txn -i
compares:
success requests (get,put,del):
put hello 1
get hello
put world 2
第一次创建 hello key,此时 keyIndex 索引为空,etcd会根据当前的全局版本号(空集群启动时默认为 1)自增,生成put hello操作对应的版本号revision{2,0},这就是boltdb的key。
boltdb的value是mvccpb.KeyValue结构体,由key、value、create_revision、mod_revision、version、lease 组成。
{
"key": "aGVsbG8=",
"create_version": 2,
"mod_version": 2,
"version": 1,
"value": "Mg=="
}
因为 key hello是首次创建,treeIndex会生成 key hello对应的keyIndex对象。
{
key: "hello"
modified: <2,0>
generations:
[
{ver:1,created:<2,0>,revisions: [<2,0>]}
]
}
再次发起一个put hello为world2修改操作时,key hello对应的keyIndex的结果如下面所示,keyIndex.modified字段更新为 <3,0>,generation的revision数组追加最新的版本号<3,0>,ver修改为2。
{
key: "hello"
modified: <3,0>
generations:
[
{ver:2,created:<2,0>,revisions: [<2,0>,<3,0>]}
]
}
读取key
在读事务中,它首先需要根据 key从treeIndex 模块获取版本号,未带版本号读,默认是读取最新的数据。
treeIndex从b-tree中,根据key查找到keyIndex对象后,匹配有效的generation,返回generation的revisions数组中最后一个版本号{2,0}给读事务,读事务根据此版本号为key,从boltdb中查询此key的value信息。
删除key
与更新key生成botldb key相比,删除key生成的boltdb key 版本号{4,0,t}追加了删除标识(tombstone, 简写t),treeIndex会给此 key hello 对应的keyIndex对象,追加一个空的generation对象,表示此索引对应的key被删除了。
{
key: "hello"
modified: <4,0>
generations:
[
{ver:3,created:<2,0>,revisions: [<2,0>,<3,0>,<4,0>(t)]},
{empty}
]
}