多版本控制:读的过程解析

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

接着上一篇多版本控制:写的过程实现继续讲解。

读过程解析

还是使用讲解键值对查询时的流程图:

image.png 读请求在底层统一调用的是 Range 方法,首先 treeIndex 根据查询的 key 从 BTree 查找对应 keyIndex 对象。从 keyIndex 结构体的定义可知,每一个 keyIndex 结构体中都包含当前键的值以及最后一次修改对应的 revision 信息,其中还保存了一个 key 的多个 generation,每一个 generation 都会存储当前 key 的所有历史版本。

treeIndex 模块中提供了 Get 接口获取一个 key 对应 revision 值:

// 位于 mvcc/index.go:68
func (ti *treeIndex) Get(key []byte, atRev int64) (modified, created revision, ver int64, err error) {
	keyi := &keyIndex{key: key}
	if keyi = ti.keyIndex(keyi); keyi == nil {
		return revision{}, revision{}, 0, ErrRevisionNotFound
	}
	return keyi.get(ti.lg, atRev)
}
复制代码

Get 接口的实现通过 keyIndex 函数查找 key 对应的 keyIndex 结构体。

// 位于 mvcc/index.go:78
func (ti *treeIndex) keyIndex(keyi *keyIndex) *keyIndex {
	if item := ti.tree.Get(keyi); item != nil {
		return item.(*keyIndex)
	}
	return nil
}
复制代码

可以看到这里的实现非常简单,从 treeIndex 成员 BTree 中查找 keyIndex,将结果转换成 keyIndex 类型后返回;获取 key 对应 revision 的实现如下:

// 位于 mvcc/key_index.go:137
func (ki *keyIndex) get(lg *zap.Logger, atRev int64) (modified, created revision, ver int64, err error) {
	if ki.isEmpty() {
		lg.Panic(
			"'get' got an unexpected empty keyIndex",
			zap.String("key", string(ki.key)),
		)
	}
	g := ki.findGeneration(atRev)
	if g.isEmpty() {
		return revision{}, revision{}, 0, ErrRevisionNotFound
	}

	n := g.walk(func(rev revision) bool { return rev.main > atRev })
	if n != -1 {
		return g.revs[n], g.created, g.ver - int64(len(g.revs)-n-1), nil
	}

	return revision{}, revision{}, 0, ErrRevisionNotFound
}
复制代码

上述实现中,遍历 generations 数组来获取 generation。匹配到有效的 generation 之后,返回 generation 的 revisions 数组中最后一个版本号,即 <3,0> 给读事务。

获取到 revision 信息之后,读事务接口优先从 buffer 中查询,如果命中则直接返回,否则根据 revision <3,0> 作为 key 在 BoltDB 中查询。

在查询时如果没有指定版本号,默认读取最新的数据。如果指定了版本号,比如我们在上面发起了一个指定历史版本号为 3 的读请求时:

$ etcdctl get hello --rev=3
复制代码

在 treeIndex 模块获取 key 对应的 keyIndex 时,指定了读版本号为 3 的快照数据。keyIndex 会遍历 generation 内的历史版本号,返回小于等于 3 的最大历史版本号作为 BoltDB 的 key,从中查询对应的 value。

需要注意的是,并发读写事务不会阻塞在一个 buffer 资源锁上。并发读创建事务时,会全量拷贝当前未提交的 buffer 数据,以此实现并发读。

小结

本课时主要介绍了 etcd 中多版本控制 MVCC 的实现。首先介绍了 MVCC 的概念,多版本并发控制可以维护一个数据的多个历史版本,且使得读写操作没有冲突。接着通过一个示例介绍了 etcd 中 MVCC 的功能。重点介绍了读写过程是如何实现多版本控制的。键值对的更新和删除都是由异步协程完成,在保证一致性的同时,也提升了读写的性能以及组件的吞吐量。

学习完本课时,给大家留一个问题,既然是批量提交,那么在提前之前出现宕机等事故时,如何保证这部分数据不会丢失的呢?欢迎你在留言区提出。

推荐阅读

  1. etcd-raft 模块如何实现分布式一致性?
  2. etcd 申请租约、绑定和撤销租约的实现解析

阅读最新文章,关注公众号:aoho求索

猜你喜欢

转载自juejin.im/post/7110395078996131847