更新文档 版本控制 多版本并发控制
三篇文章了解 TiDB 技术内幕 - 说存储| PingCAP https://pingcap.com/blog-cn/tidb-internal-1/
MVCC
很多数据库都会实现多版本控制(MVCC),TiKV 也不例外。设想这样的场景,两个 Client 同时去修改一个 Key 的 Value,如果没有 MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。 TiKV 的 MVCC 实现是通过在 Key 后面添加 Version 来实现,简单来说,没有 MVCC 之前,可以把 TiKV 看做这样的:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
有了 MVCC 之后,TiKV 的 Key 排列是这样的:
Key1-Version3 -> Value
Key1-Version2 -> Value
Key1-Version1 -> Value
……
Key2-Version4 -> Value
Key2-Version3 -> Value
Key2-Version2 -> Value
Key2-Version1 -> Value
……
KeyN-Version2 -> Value
KeyN-Version1 -> Value
……
注意,对于同一个 Key 的多个版本,我们把版本号较大的放在前面,版本号小的放在后面(回忆一下 Key-Value 一节我们介绍过的 Key 是有序的排列),这样当用户通过一个 Key + Version 来获取 Value 的时候,可以将 Key 和 Version 构造出 MVCC 的 Key,也就是 Key-Version。然后可以直接 Seek(Key-Version),定位到第一个大于等于这个 Key-Version 的位置。
Upgrade Elasticsearch | Elasticsearch Reference [6.3] | Elastic https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-upgrade.html
Clusters with a Gold or Platinum license that are upgrading to 6.3 will need to explicitly set xpack.security.enabled: true
in elasticsearch.yml
to upgrade successfully. If this value is not set, the cluster will be unable to form after upgrade.
Elasticsearch can usually be upgraded using a Rolling upgrade process so upgrading does not interrupt service. However, you might need to Reindex to upgrade indices created in older versions. Upgrades across major versions prior to 6.0 require a Full cluster restart.
When upgrading to a new version of Elasticsearch, you need to upgrade each of the products in your Elastic Stack. The steps you need to take to upgrade differ depending on which products you are using. Want a list that’s tailored to your stack? Try out our Interactive Upgrade Guide. For more information about upgrading your stack, see Upgrading the Elastic Stack.
Before upgrading Elasticsearch:
- Review the breaking changes for changes that affect your application.
- Check the deprecation log to see if you are using any deprecated features.
- If you use custom plugins, make sure compatible versions are available.
- Test upgrades in a dev environment before upgrading your production cluster.
- Back up your data before upgrading. You cannot roll back to an earlier version unless you have a backup of your data.
The following table shows when you can perform a rolling upgrade, when you need to reindex or delete old indices, and when a full cluster restart is required.
【指定版本号删除】
在更新相同标识符的文档后,版本是递增的。
文档删除时,如果声明的版本号和es中文档的版本号不一致,则操作失败。
curl -XDELETE 'localhost:9200/library/book/1' 不版本号
curl -XDELETE 'localhost:9200/library/book/1?version=1' 如指明,则需版本号存在
【使用内部、外部的版本号】
【gc】
版本信息,默认在文档删除60秒内,可用;可通过index.gc_deletes更改。
MVCC 在 etcd 中的实现 - 喵叔没话说 https://blog.betacat.io/post/mvcc-implementation-in-etcd/
MVCC 在 etcd 中的实现
简介
在数据库领域,面对高并发环境下数据冲突的问题,业界常用的解决方案有两种:
-
想办法避免冲突。使用悲观锁来确保同一时刻只有一人能对数据进行更改,常见的实现有读写锁(Read/Write Locks)、两阶段锁(Two-Phase Locking)等。
-
允许冲突,但发生冲突的时候,要有能力检测到。这种方法被称为乐观锁,即先乐观的认为冲突不会发生,除非被证明(检测到)当前确实产生冲突了。常见的实现有逻辑时钟(Logical Clock)、MVCC(Multi-version Cocurrent Control)等。
在这些解决方案中,MVCC 由于其出色的性能优势,而被越来越多的数据库所采用,比如Oracle、PostgreSQL、MySQL InnoDB、etcd 等。它的基本思想是保存一个数据的多个历史版本,从而解决事务管理中数据隔离的问题。
这里的版本一般选择使用时间戳或事务 ID 来标识。在处理一个写请求时,MVCC 不是简单的用新值覆盖旧值,而是为这一项添加一个新版本的数据。在读取一个数据项时,要先确定一个要读取的版本,然后根据版本找到对应的数据。这种写操作创建新版本,读操作访问旧版本的方式使得读写操作彼此隔离,他们之间就不需要用锁来协调。换句话说,在 MVCC 里读操作永远不会被阻塞,因此它特别适合 etcd 这种读操作比较多的场景。
MVCC 的基本原理就是这么简单,下面来结合具体代码,看看 etcd 是怎么实现的。跟上篇一样,这里的分析仍以etcd v3.3.10 为例。
数据结构
etcd 的代码中,有关 MVCC 的实现都在一个叫 mvcc 的包里。在正式介绍读写流程之前,我们先来了解下这个包里面定义的几个基础数据结构。
revision
revision 即对应到MVCC中的版本,etcd 中的每一次key-value
的操作都会有一个相应的 revision
。
|
|
这里的 main 属性对应事务 ID,全局递增不重复,它在 etcd 中被当做一个逻辑时钟来使用。sub 代表一次事务中不同的修改操作(如put和delete)编号,从0开始依次递增。所以在一次事务中,每一个修改操作所绑定的revision
依次为{txID, 0}
, {txID, 1}
, {txID, 2}
…
keyIndex
keyIndex用来记录一个key的生命周期中所涉及过的版本(revision)。
|
|
它保存的是当前key
的具体值(key),最近一次修改的版本号(modified),以及记录key
生命周期的generation
,一个generation
代表了一个key
从创建到被删除的过程。因为一个key
可能会经历创建 -> 删除 -> 创建的好几个轮回,所以它有可能会有一个或多个generation
。下面来看看一个generation
包含哪些内容:
|
|
可以看到,一个generation
中最重要的信息就是那个revision
数组,代表在它这一代中,所经历过的版本变更。
不知所云?没关系,我刚开始看这段代码的时候也是一脸懵逼,不明白为啥要这样层层包装。不过还好我们有UT,跟着UT里面的测试样例大概梳理出这样的逻辑:
- 假设我们有一个值为
foo
的key
|
|
- 为它记录两次版本变更
|
|
- 那这个
keyIndex
变成这样的结构
|
|
- 如果在此后的事务中这个key经历了被删除,再创建的过程:
|
|
- 那它会多一个
generation
|
|
好了,在知道了key的版本信息及相应的操作后,我们来看看怎么把它们组织起来:
treeIndex
treeIndex顾名思义就是一个树状索引,它通过在内存中维护一个B树,来达到加速查询key的功能。
这棵树的每一个节点都是keyIndex
,它实现了Item接口,其中的比较函数主要是比较key
的大小。这样,给定一个key
就能很快查到它对应的keyIndex
,进而可以得知它所有的版本信息。树状结构比较简单,看看图就能明白,我就不再分析了。
treeIndex结构
值得注意的是,这里只存有key的信息,value保存在磁盘里,一方面key一般比较小,同样的内存可以支持存储更多的key;另一方面,value和版本是一一对应的,内存中维护了版本的信息,也就没必要再多存一份value了。
看完了key以及key在内存中的组织结构,我们再来看看value是怎么处理的。
backend
backend封装了etcd中的存储,按照设计,backend可以对接多种存储,当前使用的是boltdb,但作为一个CNCF项目,官方也在考虑接入更多的存储引擎1。boltdb是一个纯Go语言实现的支持事务的KV存储,etcd的事务就是基于boltdb的事务实现的。
etcd在boltdb中存储的key是reversion
,value是etcd自己的key-value
组合。因此,每次更新时,新的reversion
会被记在keyIndex
中,同时,这个reversion
所对应的key-value
组合会被存到boltdb中。
举个例子: 用etcdctl通过批量接口写入两条记录:
|
|
再通过批量接口更新这两条记录:
|
|
boltdb中其实有了4条数据:
|
|
所以总的来说,内存 btree 维护的是 key 到 keyIndex 的映射,keyIndex 内维护多版本的 revision 信息,而 revision 可以映射到磁盘bbolt中具体的value。下面这个图很好的表示了这个过程2:
MVCC数据流程
基本概念就介绍到这里了,下面我们把这些概念串起来,看看读写操作的完整流程是怎样的。
写操作
首先客户端的Put
请求会被EtcdServer
的Put函数接受到,它经过一套 raft 协议的流转后,在 apply 阶段被写入,精简后的写入逻辑大概是这样的:
|
|
正如前面所分析的,这里的写入操作首先获取了当前事务开始时的ID(tw.beginRev),构造出了新的revision
,然后将 value 写入到后端的boltdb
中,并更新了内存中 key 的索引(treeIndex)。
读操作
读操作的处理函数是由EtcdServer
的Range函数处理的,在走完一段Linearizable Read
协议后,我们发现最终的操作是由storeTxnRead.rangeKeys
来完成的:
|
|
这里函数的参数curRev
代表当前事务ID。拿到了key
以及感兴趣的revision
之后,我们去treeIndex
中查询,满足指定条件的revision
有哪些,然后再根据这些revision
去boltdb
中查询当时存的key-value
是什么,并封装成RangeResult
返回出去。
小结
本文将乐观锁的原理及其在 etcd 中的实现 MVCC 梳理了一遍,希望对读者在理解和使用乐观锁上能有所帮助。
在 etcd 中,MVCC 的实现还是比较简单直接的。但在阅读代码的过程中,我也有一些看不明白的地方,毕竟一段代码在演化的过程中,会多出各种各样的细节处理,比如边界情况,性能调优等。这些细节会扰乱视线,让我们难以抓住主干。一般碰到这种情况,我会采用两种方法:
- 阅读UT,看他们都在测试什么。一般UT的输入输出都比较确定,它能够向我们揭示这段代码的作者期望完成的功能是什么。有了这个目标后我们再去看实现,就能剔出哪些是跟主干功能有关的,哪些是细节处理。
- 查找较早的提交。既然代码是演进出来的,那它刚开始的几个版本一般都比较粗糙,有很多细节没有处理,但那也是这个功能最直接的实现。通过阅读作者早期的实现,也能帮助我们快速抓住主干。