实现mvcc_一文读懂 etcd 的 mvcc 实现

提到事务必谈 ACID 特性, 基于悲观锁的实现会有读写冲突问题,性能很低,为了解决这个问题,主流数据库大多采用版本控制 mvcc[1] 技术,比如 oracle, mysql, postgresql 等等。读可以不加锁,只需要读历史版本即可 (写写还是冲突). 根据事务能看到不同版本的数据,还产生了隔离级别的问题,比如 mysql 默认的 repeatable-read, oracle 默认的 read-commited. 本文暂时只讲 mvcc, 隔离实现放到下文。

mvcc 不同数据库实现也不同,mysql 原地更新数据,将多版本保存到 undo, 而 postgresql 直接插入不同版本数据,过期的数据由 vacuum 来删除。etcd 的实现类似 pg, 本次分享看一下 etcd 的实现原理。

Revision

可以先阅读我的文章 etcd 中让人头大的 version, revision, createRevision, modRevision[2] 来了解下几个版本的概念。

type revision struct {
 // main is the main revision of a set of changes that happen atomically.
 main int64
 // sub is the sub revision of a change in a set of changes that happen
 // atomically. Each change has different increasing sub revision in that
 // set.
 sub int64
}

main 是版本 id, 逻辑时间戳全局递增。sub 表示当前事务内操作 changes 的顺序 id, 从 0 开始递增。

静态存储

etcd 的 mvcc 数据存储分两部分:内存保存所有 key 对应的版本信息,用于快速范围查询与点查,而磁盘存储所有不同版本的真实数据。

8f98dd1b711894d3f270b120bb4929f3.png

kvindex btree

内存数据由 btree 来维护,从图上可以看到,key 是用户真实的 key, value 是对应所有的版本信息。

type keyIndex struct {
 key         []byte
 modified    revision // the main rev of the last modification
 generations []generation
}

// generation contains multiple revisions of a key.
type generation struct {
 ver     int64
 created revision // when the generation is created (put in first revision).
 revs    []revision
}

keyIndex 保存 key 的所有版本信息,每删除一次都会生成一个 generation, 每个 generation 保存了这个生命周期内从创建到删除中间的所有版本号。

dce4f0b52e5f6330efa501e4fbb93465.png

磁盘 boltdb

磁盘负责存储所有数据,key 是 revision, value 是 mvccpb.KeyValue, 存储引擎是 boltdb

type KeyValue struct {
 // key is the key in bytes. An empty key is not allowed.
 Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
 // create_revision is the revision of last creation on this key.
 CreateRevision int64 `protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3" json:"create_revision,omitempty"`
 // mod_revision is the revision of last modification on this key.
 ModRevision int64 `protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3" json:"mod_revision,omitempty"`
 // version is the version of the key. A deletion resets
 // the version to zero and any modification of the key
 // increases its version.
 Version int64 `protobuf:"varint,4,opt,name=version,proto3" json:"version,omitempty"`
 // value is the value held by the key, in bytes.
 Value []byte `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
 // lease is the ID of the lease that attached to key.
 // When the attached lease expires, the key will be deleted.
 // If lease is 0, then no lease is attached to the key.
 Lease int64 `protobuf:"varint,6,opt,name=lease,proto3" json:"lease,omitempty"`
}

mvccpb.KeyValue 存储本次操作的 key, value, 还有相关的所有版本信息。

Range 查找

每次数据操作,都会在 etcdserver 层开启一个事务 txn, Range 操作是 Read 读事务,然后调用 txn 的 Range 方法,直接看 mvcc 目录下 kvstore_txn.go 文件的实现。

func (tr *storeTxnRead) Range(key, end []byte, ro RangeOptions) (r *RangeResult, err error) {
 return tr.rangeKeys(key, end, tr.Rev(), ro)
}

func (tr *storeTxnRead) rangeKeys(key, end []byte, curRev int64, ro RangeOptions) (*RangeResult, error) {
 rev := ro.Rev
 if rev > curRev {
  return &RangeResult{KVs: nil, Count: -1, Rev: curRev}, ErrFutureRev
 }
 if rev <= 0 {
  rev = curRev
 }
 if rev   return &RangeResult{KVs: nil, Count: -1, Rev: 0}, ErrCompacted
 }
  
 revpairs := tr.s.kvindex.Revisions(key, end, rev)
  ......
 kvs := make([]mvccpb.KeyValue, limit)
 revBytes := newRevBytes()
 for i, revpair := range revpairs[:len(kvs)] {
  revToBytes(revpair, revBytes)
  _, vs := tr.tx.UnsafeRange(keyBucketName, revBytes, nil, 0)
    ......
  if err := kvs[i].Unmarshal(vs[0]); err != nil {
    ......
  }
 }
 tr.trace.Step("range keys from bolt db")
 return &RangeResult{KVs: kvs, Count: len(revpairs), Rev: curRev}, nil
}

省略部份无关代码,直接看主干部份

  1. 检查所查找的 rev 版本是否有效,超过当前版本不行,被 compact 删除的也不行
  2. 根据指定版本去 kvindex 即内存 btree 中查找,所有符合 rev 版本从 key 到 end 的版本信息
  3. 遍历所有版本,UnsafeRange 去底层磁盘 boltdb 中获取真实 key/value

Put 更新数据

etcdserver 层同样要开启事务,只不过是写事务。然后实现直接看 mvcc 目录下 kvstore_txn.go

func (tw *storeTxnWrite) put(key, value []byte, leaseID lease.LeaseID) {
 rev := tw.beginRev + 1
 c := rev
 oldLease := lease.NoLease

 // if the key exists before, use its previous created and
 // get its previous leaseID
 _, created, ver, err := tw.s.kvindex.Get(key, rev)
 if err == nil {
  c = created.main
  oldLease = tw.s.le.GetLease(lease.LeaseItem{Key: string(key)})
 }
 tw.trace.Step("get key's previous created_revision and leaseID")
 ibytes := newRevBytes()
 idxRev := revision{main: rev, sub: int64(len(tw.changes))}
 revToBytes(idxRev, ibytes)

 ver = ver + 1
 kv := mvccpb.KeyValue{
  Key:            key,
  Value:          value,
  CreateRevision: c,
  ModRevision:    rev,
  Version:        ver,
  Lease:          int64(leaseID),
 }

 d, err := kv.Marshal()
  ......

 tw.tx.UnsafeSeqPut(keyBucketName, ibytes, d)
 tw.s.kvindex.Put(key, idxRev)
 tw.changes = append(tw.changes, kv)
 tw.trace.Step("store kv pair into bolt db")
  ......
}

省去不太相关的 lease 操作,只看主干逻辑

  1. 根据当前版本 key, rev 查找内存 kvindex, 看看是否有当前 key 的版本记录。主要是获取这个 key 当前的 createdRevisionVersion
  2. 生成 mvccpb.KeyValue 信息,主要是确定这次操作的 ModRevision
  3. UnsafeSeqPut 操作写磁盘 boltdb, key 是 Revision, value 是 mvccpb.KeyValue 序列化后的数据
  4. 最后更新 kvindex btree

Delete 删除

同样需要开启写事务,直接看源码

func (tw *storeTxnWrite) DeleteRange(key, end []byte) (int64, int64) {
 if n := tw.deleteRange(key, end); n != 0 || len(tw.changes) > 0 {
  return n, tw.beginRev + 1
 }
 return 0, tw.beginRev
}

func (tw *storeTxnWrite) deleteRange(key, end []byte) int64 {
 rrev := tw.beginRev
 if len(tw.changes) > 0 {
  rrev++
 }
 keys, _ := tw.s.kvindex.Range(key, end, rrev)
 if len(keys) == 0 {
  return 0
 }
 for _, key := range keys {
  tw.delete(key)
 }
 return int64(len(keys))
}

同样需要先查找内存 kvindex, 找到所有符合的待删除版本,然后调用 delete 去删

func (tw *storeTxnWrite) delete(key []byte) {
 ibytes := newRevBytes()
 idxRev := revision{main: tw.beginRev + 1, sub: int64(len(tw.changes))}
 revToBytes(idxRev, ibytes)

 if tw.storeTxnRead.s != nil && tw.storeTxnRead.s.lg != nil {
  ibytes = appendMarkTombstone(tw.storeTxnRead.s.lg, ibytes)
 } else {
  // TODO: remove this in v3.5
  ibytes = appendMarkTombstone(nil, ibytes)
 }

 kv := mvccpb.KeyValue{Key: key}

 d, err := kv.Marshal()
 if err != nil {
  if tw.storeTxnRead.s.lg != nil {
   tw.storeTxnRead.s.lg.Fatal(
    "failed to marshal mvccpb.KeyValue",
    zap.Error(err),
   )
  } else {
   plog.Fatalf("cannot marshal event: %v", err)
  }
 }

 tw.tx.UnsafeSeqPut(keyBucketName, ibytes, d)
 err = tw.s.kvindex.Tombstone(key, idxRev)
 ......
}
  1. 生成 ibytes, 然后追加一个 appendMarkTombstone 标记,表示这个 revision 是 delete,并且生成一个只含有 key 的 mvccpb.KeyValue
  2. UnsafeSeqPut 删除磁盘 boltdb, 注意这里底层只是标记删除,磁盘空间并不释放
  3. Tombstone 结束当前生命周期,生成一个新的空 generation, 更新 kvindex

数据过期

与 pg 比较像,过期与删除数据都是惰性删除的。etcd 可以配置只保留固定时间的数据,所以会周期性的 Compact. 同样分为两部分,内存 btree 数据如果发现当前 generation 为空,并且最大 Revision 己过期,那就从 btree 中删除。

磁盘数据由 boltdb 维护,只会标记为删除,磁盘空间可以回收利用,但是不会自动释放,只有调用 Defrag 才会重建磁盘文件。

另外说到存储引擎 boltdb, 这个东西性能一般,除了 etcd 没有什么知名项目在用。读事务是并发,但是写事务是串行的,所以内部会将尽可能多的写入 batch 一起操作,异步提交。

小结

这次分享就这些,以后面还会分享更多关于 etcd 的内容,如果感兴趣,可以关注并转发(:

参考资料

[1]

什么是 mvcc: https://en.wikipedia.org/wiki/Multiversion_concurrency_control,

[2]

etcd 中让人头大的 version, revision, createRevision, modRevision: https://mp.weixin.qq.com/s/TFcSEBBMnb0wJ_A3R4Jfqw,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/244189.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pygame 粒子物理:Numba实现同时渲染十万+像素

图中同时渲染了十万个像素&#xff0c;没有明显掉帧 我对Pygame的印象一直是慢的扣脚的&#xff0c;直到前段时间看到了一段MandelBrot代码&#xff08;源地址弄丢了&#xff09;其中使用了这个功能:pygame.surfarray.make_surface() 这里可以直接把numpy阵列转换为pygame.su…

“vector”: 不是“std”的成员_libcxx 的 std::function 源码分析

链接&#xff1a;functional。其中 std::function 的主体内容在 2100 多行。先来看 function 的头部。template<class _Rp, class ..._ArgTypes> class _LIBCPP_TEMPLATE_VIS function<_Rp(_ArgTypes...)>: public __function::__maybe_derive_from_unary_function…

python验证身份证号码大全_身份证号码处理技巧大全

身份证号码处理技巧大全&#xff0c;汇总了常用的身份证号码处理六大技巧&#xff1a;不需要复杂的公式&#xff0c;点点鼠标即可完成&#xff0c;简单快捷&#xff0c;下面将详细介绍六大功能的具体用法。(文章最后有工具和演示文件的下载地址&#xff0c;可以下载下来同步操作…

语言print如何实现连续输出_【每日一题】如何实现一个高效的单向链表逆序输出?...

今后&#xff0c;动力节点Java学院将每天为大家带来一道大厂面试真题&#xff0c;这些面试题都是大厂技术专家们结合多年的工作、面试经验总结提炼而成的面试真题。通过这些面试题&#xff0c;还可以间接地了解技术大牛们出题思路与考察要点。建议大家收藏并分享给更多需要的人…

恩尼格玛模拟器_用C语言编的恩格尼码模拟器

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼void enumio(char pie1[],char pie2[],char pie3[],char pier[],char ch0[],char chz[],char ip[],char k[],int cou){ int check(char *a);int excheck(char *a);int compare(char *le,char *unle);int factorial(int n);void cyc…

louvain算法python_复杂网络任务6:Louvain社区发现算法的原理、细节和实现,作业,六,以及...

ΔQ[∑in2∗mki,in2∗m−(∑tot2∗m)2−(2∗∑tot∗ki4∗m2)−(ki2∗m)2]−[∑in2∗m−(∑tot2∗m)2−(ki2∗m)2]ki,in2∗m−2∗∑tot∗ki4∗m212∗m∗(ki,in−∑tot∗kim)\Delta{Q} [\frac{\sum_{in}} {2*m} \frac{k_{i,in}}{2*m} - (\frac{\sum_{tot}}{2*m})^2 - (\frac{2*…

系统相机裁剪比例_从单反到手机,三种黄金比例构图方法,让你的照片与众不同...

古埃及金字塔和达芬奇蒙娜丽莎有什么共同之处&#xff1f;它们都是使用黄金比例进行设计的。不管是建筑设计还是绘画&#xff0c;它们都是属于艺术的一种&#xff0c;所以黄金比例也同样适用于摄影构图中。很多优秀的摄影作品都会使用黄金比例的构图方法进行拍摄&#xff0c;因…

mysql安装图解_MySQL安装图解

目录一、安装详细过程MySQL默认安装在“C:\Program Files”目录下。普通使用只安装MySQL Server就足够了&#xff0c;大小为416M。如果不想装在C盘&#xff0c;也可以安装完成之后再将其移动到其他盘。1.接受许可&#xff0c;点击Next2.选择安装功能&#xff0c;推荐选择Server…

mysql字符集设置_mysql字符集设置

配置文件路径&#xff1a; /full/path/mysql/bin/my.cnf (默认为/etc/my.cnf )[client]default-character-setutf8[mysql]default-character-setutf8[mysqld]init_connectSET collation_connection utf8_unicode_ciinit_connectSET NAMES utf8character-set-serverutf8collati…

mysql 索引 原理_MySQL——索引实现原理

在MySQL中&#xff0c;索引属于存储引擎级别的概念&#xff0c;不同存储引擎对索引的实现方式是不同的&#xff0c;本文主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。MyISAM索引实现MyISAM引擎使用BTree作为索引结构。MyISAM会按照数据插入的顺序分配行号&#xff0c;从…

mysql 字段 中文_如何配置mysql支持中文字段名与中文字段

匿名用户1级2018-11-18 回答中文字段名都可以了 但是中文记录不行 奇怪啊mysql>; create table a (a char(20));Query OK, 0 rows affected (0.05 sec)mysql>; insert into a values(^_^);Query OK, 1 row affected (0.05 sec)mysql>; insert into a values(中guo);Qu…

mysql中如何删除多个表格_mysql怎么批量删除多个表?

mysql批量删除多个表的方法&#xff1a;使用“DROP TABLE”语句&#xff0c;只要将表名依次写在后面&#xff0c;相互之间用逗号隔开即可&#xff1b;语法格式“DROP TABLE [IF EXISTS] 表名1 [ ,表名2, 表名3 ...]”。mysql批量删除多个表使用 DROP TABLE 语句可以删除一个或多…

mysql 图片 格式_mysql存储图片 用什么格式

{"moduleinfo":{"card_count":[{"count_phone":1,"count":1}],"search_count":[{"count_phone":4,"count":4}]},"card":[{"des":"阿里云数据库专家保驾护航&#xff0c;为用户…

mysql5.5更改端口后初始化_centos7 修改mysql5.7默认端口后启动异常

关闭selinux的方法有两种&#xff1a;临时关闭和永久关闭。查看selinux的状态&#xff1a;sestatus[root162-219-29-3 ~]# sestatusSELinux status: enabledSELinuxfs mount: /sys/fs/selinuxSELinux root directory: /etc/selinuxLoaded policy name: targetedCurrent mode: e…

开启mysql日志记录_Mysql开启日志记录

vim /etc/my.conf.d/server.cnf:#lower_case_tables_name 1#错误日志log_error /var/log/mysql_error.log#慢查询日志slow_query_logONslow_query_log_file/mnt/lnx_log/mysql/slow.loglong_query_time1 #单位秒&#xff0c;超过此值则记录为慢查询#通用查询日志&#xff0c;…

mysql存储引擎innodb_MySQL常用存储引擎之Innodb

1. mysql 5.5及之后版本默认存储引擎为不了解存储引擎的数据库使用者&#xff0c;提供了很大的便利&#xff0c;因为innodb适应大部分应用场景。和myisam不同的是&#xff0c;innodb是一种事务型存储引擎。也就是说&#xff0c;innodb是支持事务的acid特性的。innodb的设计&…

mysql 的驱动是多少_mysql驱动参数变化

在java平台使用的mysql jdbc驱动为&#xff1a;mysql-connector-java。在项目中添加如下依赖&#xff1a;mysqlmysql-connector-java${version.mysql.connector}在6.0.2版本之前drivercom.mysql.jdbc.Driverurljdbc:mysql://host:port/dbname?characterEncodingutf8在6.0.2版本…

mysql 对已有表分区_mysql怎么对先有表进行分区

mysql如何对先有表进行分区&#xff1f;有一张表&#xff0c;已经创建了&#xff0c;是一张普通的表&#xff0c;先要对这张表进行hash分区&#xff0c;我用一下语句创建提示错误&#xff1a;mysql> alter table 33-> partition by hash(id)-> partitions 2;ERROR 106…

如何查看mysql my.ini_MySQL学习笔记(一)

MySQL 学习笔记一.安装Typical:经典安装Custom:自定义安装Complete: 完全安装二.修改编码方式[mysqld]myini:character-set-serverutf8[client] port3306 default-character-setutf8三.启动停止mysql服务cmd net stop mysqlcmd net start mysql四.MySQL登录mysql -V 查看版本&…

springboot quartz 动态配置_springboot集成quartz实现动态任务调度

quartz是一个开源的作业调度框架&#xff0c;本文就是介绍下springboot框架下继承quartz的一些使用示例首先我们需要添加quartz的spring-boot-starter-quartz依赖org.springframework.bootspring-boot-starter-quartz我们需要做一些配置。quartz提供了基于内存(MEMORY)和基于jd…