存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎

哈希存储引擎

哈希存储引擎哈希表的持久化实现,支持增、删、改以及随机读取操作,但不支持顺序扫描,对应的存储系统为key-value存储系统。对于key-value的插入以及查询,哈希表的复杂度都是O(1),明显比树的操作O(n)快,如果不需要有序的遍历数据,哈希表就非常适合。代表性的数据库有:Redis,Memcache,以及存储系统Bitcask。
Bitcask是一个基于哈希表结构的键值存储系统,它仅支持追加操作(Append-only),所有的写操作只追加不修改老的数据。每个文件有一定的大小限制,当文件增加到相应大小,就会产生一个新文件,老的文件只读不写。在任意时刻,只有一个文件是可写的,用于数据追加,称为活跃文件,而其他已经达到大小限制的文件,称为老数据文件。

数据结构

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

Bitcask数据文件中的数据是一条一条写入操作,记录包含,key,value,主键长度,value长度,时间戳(timestamp)以及crc校验值。(删除操作不会删除旧的条目,而是将value设定为一个特殊的标识值)。
内存中采用基于哈希表的索引数据结构,哈希表的作用是通过主键快速地定位到value的位置。哈希表结构中的每一项包含了三个用于定位数据的信息,分别是文件编号(file id),value在文件中的位置(value_pos),value长度(value_sz),通过读取file_id对应文件的value_pos开始的value_sz个字节,这就得到了最终的value值。写入时首先将Key-Value记录追加到活跃数据文件的末尾,接着更新内存哈希表,因此,每个写操作总共需要进行一次顺序的磁盘写入和一次内存操作。
Bitcask在内存中存储了主键和value的索引信息,磁盘文件中存储了主键和value的实际内容。系统基于一个假设,value的长度远大于主键的长度。假如value的平均长度为1KB,每条记录在内存中的索引信息为32字节,那么,磁盘内存比为32 : 1。这样,32GB内存索引的数据量为32GB×32 = 1TB。

定期合并

Bitcask系统中的记录删除或者更新后,原来的记录成为垃圾数据。如果这些数据一直保存下去,文件会无限膨胀下去,为了解决这个问题,Bitcask需要定期执行合并(Compaction)操作以实现垃圾回收。所谓合并操作,即将所有老数据文件中的数据扫描一遍并生成新的数据文件,这里的合并其实就是对同一个key的多个操作以只保留最新一个的原则进行删除,每次合并后,新生成的数据文件就不再有冗余数据了。

快速恢复

Bitcask系统中的哈希索引存储在内存中,如果不做额外的工作,服务器断电重启重建哈希表需要扫描一遍数据文件,如果数据文件很大,这是一个非常耗时的过程。Bitcask通过索引文件(hint file)来提高重建哈希表的速度。
简单来说,索引文件就是将内存中的哈希索引表转储到磁盘生成的结果文件。Bitcask对老数据文件进行合并操作时,会产生新的数据文件,这个过程中还会产生一个索引文件,这个索引文件记录每一条记录的哈希索引信息。与数据文件不同的是,索引文件并不存储具体的value值,只存储value的位置(与内存哈希表一样)。这样,在重建哈希表时,就不需要扫描所有数据文件,而仅仅需要将索引文件中的数据一行行读取并重建即可,大大减少了重启后的恢复时间。

B树存储引擎

相比哈希存储引擎,B树存储引擎不仅支持随机读取,还支持范围扫描。关系数据库中通过索引访问数据,在Mysql InnoDB中,有一个称为聚集索引的特殊索引,行的数据存于其中,组织成B+树(B树的一种)数据结构。

1.数据结构

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

如图所示,MySQL InnoDB按照页面(Page)来组织数据,每个页面对应B+树的一个节点。其中,叶子节点保存每行的完整数据,非叶子节点保存索引信息。数据在每个节点中有序存储,数据库查询时需要从根节点开始二分查找直到叶子节点,每次读取一个节点,如果对应的页面不在内存中,需要从磁盘中读取并缓存起来。B+树的根节点是常驻内存的,因此,B+树一次检索最多需要h-1次磁盘IO,复杂度为O(h)=O(logdN)(N为元素个数,d为每个节点的出度,h为B+树高度)。修改操作首先需要记录提交日志,接着修改内存中的B+树。如果内存中的被修改过的页面超过一定的比率,后台线程会将这些页面刷到磁盘中持久化。

2.缓冲区管理

缓冲区管理器负责将可用的内存划分成缓冲区,缓冲区是与页面同等大小的区域,磁盘块的内容可以传送到缓冲区中。缓冲区管理器的关键在于替换策略,即选择将哪些页面淘汰出缓冲池。常见的算法有以下两种。

(1)LRU

LRU算法淘汰最长时间没有读或者写过的块。这种方法要求缓冲区管理器按照页面最后一次被访问的时间组成一个链表,每次淘汰链表尾部的页面。直觉上,长时间没有读写的页面比那些最近访问过的页面有更小的最近访问的可能性。

(2)LIRS

LRU算法在大多数情况下表现是不错的,但有一个问题:假如某一个查询做了一次全表扫描,将导致缓冲池中的大量页面(可能包含很多很快被访问的热点页面)被替换,从而污染缓冲池。现代数据库一般采用LIRS算法,将缓冲池分为两级,数据首先进入第一级,如果数据在较短的时间内被访问两次或者以上,则成为热点数据进入第二级,每一级内部还是采用LRU替换算法。Oracle数据库中的Touch Count算法和MySQL InnoDB中的替换算法都采用了类似的分级思想。以MySQL InnoDB为例,InnoDB内部的LRU链表分为两部分:新子链表(new sublist)和老子链表(old sublist),默认情况下,前者占5/8,后者占3/8。页面首先插入到老子链表,InnoDB要求页面在老子链表停留时间超过一定值,比如1秒,才有可能被转移到新子链表。当出现全表扫描时,InnoDB将数据页面载入到老子链表,由于数据页面在老子链表中的停留时间不够,不会被转移到新子链表中,这就避免了新子链表中的页面被替换出去的情况。

插入(insert)操作

插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。如下图所示:
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

1、OK,下面咱们通过一个实例来逐步讲解下。插入以下字符字母到一棵空的树中(非根结点关键字数小了(小于2个)就合并,大了(超过4个)就分裂):C N G A H E K Q M F W L T Z D P R X Y S,首先,结点空间足够,4个字母插入相同的结点中,如下图:
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

2、当咱们试着插入H时,结点发现空间不够,以致将其分裂成2个结点,移动中间元素G上移到新的根结点中,在实现过程中,咱们把AC留在当前结点中,而HN放置新的其右邻居结点中。如下图:
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

3、当咱们插入E,K,Q时,不需要任何分裂操作
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

4、插入M需要一次分裂,注意M恰好是中间关键字元素,以致向上移到父节点中
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

5、插入F,W,L,T不需要任何分裂操作
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

6、插入Z时,最右的叶子结点空间满了,需要进行分裂操作,中间元素T上移到父节点中,注意通过上移中间元素,树最终还是保持平衡,分裂结果的结点存在2个关键字元素。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

7、插入D时,导致最左边的叶子结点被分裂,D恰好也是中间元素,上移到父节点中,然后字母P,R,X,Y陆续插入不需要任何分裂操作(别忘了,树中至多5个孩子)。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

8、最后,当插入S时,含有N,P,Q,R的结点需要分裂,把中间元素Q上移到父节点中,但是情况来了,父节点中空间已经满了,所以也要进行分裂,将父节点中的中间元素M上移到新形成的根结点中,注意以前在父节点中的第三个指针在修改后包括DG节点中。这样具体插入操作的完成,下面介绍删除操作,删除操作相对于插入操作要考虑的情况多点。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》


删除(delete)操作


首先查找
B
树中需删除的元素
,
如果该元素在
B
树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素(“左孩子最右边的节点”

“右孩子最左边的节点”)到父节点中,然后

移动之后

情况;如果没有,直接
删除后,
移动之后的情况

删除元素,移动相应元素之后,
如果某结点中元素数目(即关键字数)小于
ceil(m/2)-1
,则需要看其某相邻兄弟结点是否丰满(结点中元素个数大于ceil(m/2)-1)(
还记得第一节中关于B树的第5个特性中的c点么?: c)除根结点之外的结点(包括叶子结点)的关键字的个数n必须满足: (ceil(m / 2)-1)<= n <= m-1。m表示最多含有m个孩子,n表示关键字数。在本小节中举的一颗B树的示例中,关键字数n满足:2<=n<=4
),如果丰满,则向父节点借一个元素来满足条件;如果其相邻兄弟都刚脱贫,即借了之后其结点数目小于ceil(m/2)-1
,则该结点与其相邻的某一兄弟结点进行

合并
”成一个结点,以此来满足条件。那咱们通过下面实例来详细了解吧。
以上述插入操作构造的一棵5阶B
树(树中最多含有m(m=5)个孩子,因此关键字数最小为
ceil(m / 2)-1=2。
还是这句话,
关键字数
小了(小于2个)就合并,大了(超过4个)就分裂
)为例,依次删除
H,T,R,E

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

1、首先删除元素H,当然首先查找HH在一个叶子结点中,且该叶子结点元素数目3大于最小元素数目ceil(m/2)-1=2,则操作很简单,咱们只需要移动K至原来H的位置,移动LK的位置(也就是结点中删除元素后面的元素向前移动)
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

2、下一步,删除T,因为T没有在叶子结点中,而是在中间结点中找到,咱们发现他的继承者W(字母升序的下个元素),将W上移到T的位置,然后将原包含W的孩子结点中的W进行删除,这里恰好删除W后,该孩子结点中元素个数大于2,无需进行合并操作。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

3、下一步删除RR在叶子结点中,但是该结点中元素数目为2,删除导致只有1个元素,已经小于最小元素数目ceil(5/2)-1=2,而由前面我们已经知道:如果其某个相邻兄弟结点中比较丰满(元素个数大于ceil(5/2)-1=2),则可以向父结点借一个元素,然后将最丰满的相邻兄弟结点中上移最后或最前一个元素到父节点中(有没有看到红黑树中左旋操作的影子?),在这个实例中,右相邻兄弟结点中比较丰满(3个元素大于2),所以先向父节点借一个元素W下移到该叶子结点中,代替原来S的位置,S前移;然后X在相邻右兄弟结点中上移到父结点中,最后在相邻右兄弟结点中删除X,后面元素前移。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

4、最后一步删除E, 删除后会导致很多问题,因为E所在的结点数目刚好达标,刚好满足最小元素个数(ceil(5/2)-1=2),而相邻的兄弟结点也是同样的情况,删除一个元素都不能满足条件,所以需要该节点与某相邻兄弟结点进行合并操作;首先移动父结点中的元素(该元素在两个需要合并的两个结点元素之间)下移到其子结点中,然后将这两个结点进行合并成一个结点。所以在该实例中,咱们首先将父节点中的元素D下移到已经删除E而只有F的结点中,然后将含有DF的结点和含有A,C的相邻兄弟结点进行合并成一个结点。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

5、也许你认为这样删除操作已经结束了,其实不然,在看看上图,对于这种特殊情况,你立即会发现父节点只包含一个元素G,没达标(因为非根节点包括叶子结点的关键字数n必须满足于2=<n<=4,而此处的n=1),这是不能够接受的。如果这个问题结点的相邻兄弟比较丰满,则可以向父结点借一个元素。假设这时右兄弟结点(含有Q,X)有一个以上的元素(Q右边还有元素),然后咱们将M下移到元素很少的子结点中,将Q上移到M的位置,这时,Q的左子树将变成M的右子树,也就是含有NP结点被依附在M的右指针上。所以在这个实例中,咱们没有办法去借一个元素,只能与兄弟结点进行合并成一个结点,而根结点中的唯一元素M下移到子结点,这样,树的高度减少一层。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》
为了进一步详细讨论删除的情况,
再举另外一个实例

这里是一棵不同的
5

B
树,那咱们试着删除
C
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

于是将删除元素C的右子结点中的D元素上移到C的位置,但是出现上移元素后,只有一个元素的结点的情况。
又因为含有E的结点,其相邻兄弟结点才刚脱贫(最少元素个数为2),不可能向父节点借元素,所以只能进行合并操作,于是这里将含有A,B的左兄弟结点和含有E的结点进行合并成一个结点。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

这样又出现只含有一个元素F结点的情况,这时,其相邻的兄弟结点是丰满的(元素个数为3>最小元素个数2),这样就可以想父结点借元素了,把父结点中的J下移到该结点中,相应的如果结点中J后有元素则前移,然后相邻兄弟结点中的第一个元素(或者最后一个元素)上移到父节点中,后面的元素(或者前面的元素)前移(或者后移);注意含有KL的结点以前依附在M的左边,现在变为依附在J的右边。这样每个结点都满足B树结构性质。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

从以上操作可看出:除根结点之外的结点(包括叶子结点)的关键字的个数n满足:(ceil(m / 2)-1)<= n <= m-1,即2<=n<=4。这也佐证了咱们之前的观点。删除操作完。

LSM树存储引擎

LSM树(Log Structured Merge Tree)的思想非常朴素,就是将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘,读取时需要合并磁盘中的历史数据和内存中最近的修改操作。LSM树的优势在于有效地规避了磁盘随机写入问题,但读取时可能需要访问较多的磁盘文件。

1.存储结构

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

如图2-8所示,LevelDB存储引擎主要包括:内存中的MemTable和不可变MemTable(Immutable MemTable,也称为Frozen MemTable,即冻结MemTable)以及磁盘上的几种主要文件:当前(Current)文件、清单(Manifest)文件、操作日志(Commit Log,也称为提交日志)文件以及SSTable文件。当应用写入一条记录时,LevelDB会首先将修改操作写入到操作日志文件,成功后再将修改操作应用到MemTable,这样就完成了写入操作。
当MemTable占用的内存达到一个上限值后,需要将内存的数据转储到外存文件中。LevelDB会将原先的MemTable冻结成为不可变MemTable,并生成一个新的MemTable。新到来的数据被记入新的操作日志文件和新生成的MemTable中。顾名思义,不可变 MemTable的内容是不可更改的,只能读取不能写入或者删除。LevelDB后台线程会将不可变MemTable的数据排序后转储到磁盘,形成一个新的SSTable文件,这个操作称为Compaction。SSTable文件是内存中的数据不断进行Compaction操作后形成的,且SSTable的所有文件是一种层级结构,第0层为Level 0,第1层为Level 1,以此类推。
SSTable中的文件是按照记录的主键排序的,每个文件有最小的主键和最大的主键。LevelDB的清单文件记录了这些元数据,包括属于哪个层级、文件名称、最小主键和最大主键。当前文件记录了当前使用的清单文件名。在LevelDB的运行过程中,随着Compaction的进行,SSTable文件会发生变化,新的文件会产生,老的文件被废弃,此时往往会生成新的清单文件来记载这种变化,而当前文件则用来指出哪个清单文件才是当前有效的。
直观上,LevelDB每次查询都需要从老到新读取每个层级的SSTable文件以及内存中的MemTable。LevelDB做了一个优化,由于LevelDB对外只支持随机读取单条记录,查询时LevelDB首先会去查看内存中的MemTable,如果MemTable包含记录的主键及其对应的值,则返回记录即可;如果MemTable没有读到该主键,则接下来到同样处于内存中的不可变Memtable中去读取;类似地,如果还是没有读到,只能依次从新到老读取磁盘中的SSTable文件。

WAL: Write-Ahead Logging

在设计数据库的时候经常被使用,当插入一条数据时,数据先顺序写入 WAL 文件中,之后插入到内存中的 MemTable 中。这样就保证了数据的持久化,不会丢失数据,并且都是顺序写,速度很快。当程序挂掉重启时,可以从 WAL 文件中重新恢复内存中的

MemTable

MemTable 对应的就是 WAL 文件,是该文件内容在内存中的存储结构,通常用 SkipList 来实现。MemTable 提供了 k-v 数据的写入、删除以及读取的操作接口。其内部将 k-v 对按照 key 值有序存储,这样方便之后快速序列化到 SSTable 文件中,仍然保持数据的有序性。

Immutable Memtable

顾名思义,Immutable Memtable 就是在内存中只读的 MemTable,由于内存是有限的,通常我们会设置一个阀值,当 MemTable 占用的内存达到阀值后就自动转换为 Immutable Memtable,Immutable Memtable 和 MemTable 的区别就是它是只读的,系统此时会生成新的 MemTable 供写操作继续写入。
之所以要使用 Immutable Memtable,就是为了避免将 MemTable 中的内容序列化到磁盘中时会阻塞写操作。

SSTable

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

SSTable 就是 MemTable 中的数据在磁盘上的有序存储,其内部数据是根据 key 从小到大排列的。通常为了加快查找的速度,需要在 SSTable 中加入数据索引,可以快读定位到指定的 k-v 数据。
SSTable 通常采用的分级的结构,例如 LevelDB 中就是如此。MemTable 中的数据达到指定阀值后会在 Level 0 层创建一个新的 SSTable。当某个 Level 下的文件数超过一定值后,就会将这个 Level 下的一个 SSTable 文件和更高一级的 SSTable 文件合并,由于 SSTable 中的 k-v 数据都是有序的,相当于是一个多路归并排序,所以合并操作相当快速,最终生成一个新的 SSTable 文件,将旧的文件删除,这样就完成了一次合并过程。

2.合并

LevelDB写入操作很简单,但是读取操作比较复杂,需要在内存以及各个层级文件中按照从新到老依次查找,代价很高。为了加快读取速度,LevelDB内部会执行Compaction操作来对已有的记录进行整理压缩,从而删除一些不再有效的记录,减少数据规模和文件数量。
LevelDB的Compaction操作分为两种:minor compaction和major compaction。Minor compaction是指当内存中的MemTable大小到了一定值时,将内存数据转储到SSTable文件中。每个层级下有多个SSTable,当某个层级下的SSTable文件数目超过一定设置值后,levelDB会从这个层级中选择SSTable文件,将其和高一层级的SSTable文件合并,这就是major compaction。major compaction相当于执行一次多路归并:按照主键顺序依次迭代出所有SSTable文件中的记录,如果没有保存价值,则直接抛弃;否则,将其写入到新生成的SSTable文件中。

常用操作的实现

写入

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》


在 LSM Tree 中,写入操作是相当快速的,只需要在 WAL 文件中顺序写入当次操作的内容,成功之后将该 k-v 数据写入 MemTable 中即可。尽管做了一次磁盘 IO,但是由于是顺序追加写入操作,效率相对来说很高,并不会导致写入速度的降低。数据写入 MemTable 中其实就是往 SkipList 中插入一条数据,过程也相当简单快速。

更新

更新操作其实并不真正存在,和写入一个 k-v 数据没有什么不同,只是在读取的时候,会从 Level0 层的 SSTable 文件开始查找数据,数据在低层的 SSTable 文件中必然比高层的文件中要新,所以总能读取到最新的那条数据。也就是说此时在整个 LSM Tree 中可能会同时存在多个 key 值相同的数据,只有在之后合并 SSTable 文件的时候,才会将旧的值删除。

删除

删除一条记录的操作比较特殊,并不立即将数据从文件中删除,而是记录下对这个 key 的删除操作标记,同插入操作相同,插入操作插入的是 k-v 值,而删除操作插入的是 k-del 标记,只有当合并 SSTable 文件时才会真正的删除。

Compaction

当数据不断从 Immutable Memtable 序列化到磁盘上的 SSTable 文件中时,SSTable 文件的数量就不断增加,而且其中可能有很多更新和删除操作并不立即对文件进行操作,而只是存储一个操作记录,这就造成了整个 LSM Tree 中可能有大量相同 key 值的数据,占据了磁盘空间。
为了节省磁盘空间占用,控制 SSTable 文件数量,需要将多个 SSTable 文件进行合并,生成一个新的 SSTable 文件。比如说有 5 个 10 行的 SSTable 文件要合并成 1 个 50 行的 SSTable 文件,但是其中可能有 key 值重复的数据,我们只需要保留其中最新的一条即可,这个时候新生成的 SSTable 可能只有 40 行记录。
通常在使用过程中我们采用分级合并的方法,其特点如下:

  1. 每一层都包含大量 SSTable 文件,key 值范围不重复,这样查询操作只需要查询这一层的一个文件即可。(第一层比较特殊,key 值可能落在多个文件中,并不适用于此特性)
  2. 当一层的文件达到指定数量后,其中的一个文件会被合并进入上一层的文件中。

读取

《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

LSM Tree 的读取效率并不高,当需要读取指定 key 的数据时,先在内存中的 MemTable 和 Immutable MemTable 中查找,如果没有找到,则继续从 Level 0 层开始,找不到就从更高层的 SSTable 文件中查找,如果查找失败,说明整个 LSM Tree 中都不存在这个 key 的数据。如果中间在任何一个地方找到这个 key 的数据,那么按照这个路径找到的数据都是最新的。
在每一层的 SSTable 文件的 key 值范围是不重复的,所以只需要查找其中一个 SSTable 文件即可确定指定 key 的数据是否存在于这一层中。Level 0 层比较特殊,因为数据是 Immutable MemTable 直接写入此层的,所以 Level 0 层的 SSTable 文件的 key 值范围可能存在重复,查找数据时有可能需要查找多个文件。

优化读取

因为这样的读取效率非常差,通常会进行一些优化,例如 LevelDB 中的 Mainfest 文件,这个文件记录了 SSTable 文件的一些关键信息,例如 Level 层数,文件名,最小 key 值,最大 key 值等,这个文件通常不会太大,可以放入内存中,可以帮助快速定位到要查询的 SSTable 文件,避免频繁读取。
另外一个经常使用的方法是布隆解析器(Bloom filter),布隆解析器是一个使用内存判断文件是否包含一个关键字的有效方法。

并发控制

1、数据库锁

读锁,写锁

2、写时复制

读事务不用加锁
B+树写时复制执行写操作的步骤:

  1. 拷贝:将从叶子到根节点路径上的所有节点拷贝出来。
  2. 修改:对拷贝的节点执行修改。
  3. 提交:原子地切换根节点的指针,使之指向新的根节点。

写时复制技术涉及引用计数,对每个节点维护一个引用计数,表示被多少节点引用,如果引用计数变为0,说明没有节点引用,可以被垃圾回收。
缺点

  1. 每次写操作都要拷贝从叶子节点到根节点路径上的所有节点,写操作成本高。
  2. 多个写操作之间是互斥的,同一时刻只允许一个写操作。

3、多版本并发控制(MVCC)

读事务不用加锁
InnoDB存储引擎:
对每一行维护了两个隐含的列,一列为行被修改的“时间”,一列为行被删除的“时间”,“时间”即为版本号。
每当一个事务开始时,InnoDB都会给这个事务分配一个递增的版本号(事务号)。
对于每一行查询语句,InnoDB都会把这个查询语句的版本号同这个查询语句遇到的行的版本号对比,然后结合不同的事务隔离级别,来决定是否返回改行。

(1)、SELECT

对于SELECT语句,只有同时满足下面两个条件的行,才能被返回:
a) 行的修改版本号小于等于该事务号
b) 行的删除版本号要么没有被定义,要么大于事务的版本号

(2)、INSERT

对于新插入的行,行的修改版本号更新为该事务的事务号。

(3)、DELETE

对于删除,InnoDB直接把改行的删除版本号设置为当前的事务号,相当于标记为删除,而不是物理删除。

(4)、UPDATE

在更新行的时候,InnoDB会把原来的行复制一份,并把当前的事务号作为改行的修改版本号。

存储快照实现原理

存储快照有两种实现方式:COW(写时复制Copy-On-Write)、ROW(写重定向Redirect-On-Write),两种实现方法有区别,造成读写性能、应用场景有比较大的区别。

COW:

原理见下图(从网上找的,没自己画)。
1)原卷数据是A~G。此卷Metedata像指针一样指向这些数据。
2)当做快照时,重新复制一份Metedata,并且也指向这些A~G数据。
3)当有数据要写入到源卷时(下图写入D’),写入到D的原位置之前,需要把D拷贝出放到一个新位置。然后修改快照的Metedata的其中一个指针指向拷贝出的位置[D](图中是Snapshot data的存储位置)。同时,把D’写入到D原来的位置。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

此方式可以看出,源卷的Metedata的是没有变化的。对原卷是连续的数据,多次快照,多次写之后还是连续的数据,因此读性能或者对单个位置的多次写性能都不会有很大的影响。
但是,快照的数据是非连续的,如数据ABCEFG还是在源卷的位置,是连续数据。而数据D在存储的其他位置,非连续。 如果多次快照,不同位置的多次读写后,快照的数据可能就比较混乱。造成对快照的读写延时较大。
应用场景:
这种实现方式在第一次写入某个存储位置时需要完成一个读操作(读原位置的数据),两个写操作(写原位置与写快照空间),如果写入频繁,那么这种方式将非常 消耗IO时间。因此可推断,如果预计某个卷上的I/O多数以读操作为主,写操作较少的场景,这种方式的快照实现技术是一个较理想的选择,因为快照的完成需要较少 的时间。除此之外,如果一个应用易出现写入热点,即只针对某个有限范围内的数据进行写操作,那么COW的快照实现方式也是较较理想的选择。因为其数据更改都局限在一个范围内,对同一份数据的多次写操作只会出现一次写时复制操作。
但是这种方式的缺点也是非常明显的。如果写操作过于分散且频繁,那么 COW造成的开销则是不可忽略的,有时甚至是无法接受的。因此在应用时,则需要综合评估应用系统的使用场景,以判断这种方式的快照是否适用。

ROW:

原理见下图:
Vd是源卷的Metedata,分别指向4块数据。做快照时,snap的metadata也指向此4块数据。当有数据写入时,把数据写入另外一个位置,然后修改vd的其中一个metedata到新位置。snap的metedata数据不变化。
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

此方式源卷经过长时间写后,所有数据块的位置可能都会重定向到其他位置,导致源卷的数据不连续。在集中式存储情况下,会导致对源卷的读写性能降低。


两种方式的优缺点,及应用场景:
COW最大的问题是对写性能有影响。第一次修改原卷,需要复制数据,因此需要多一次读写的数据块迁移过程。这个就比较要命,应用需要等待时间比较长。但原卷数据的布局没有任何改变,因此对读性能没有任何影响。
ROW在传统存储情况下最大的问题是对读性能影响比较大。ROW写的时候性能基本没有损耗,只是修改指针,实现效率很高。但多次读写后,原卷的数据就分散到各个地方,对于连续读写的性能不如COW。这种方式比较适合Write-Intensive(写密集)类型的存储系统
但是,在分布式存储的情况下,ROW的连续读写的性能会比COW差吗? 就不一定了。正常情况下,读写性能的瓶颈一般是在磁盘上。分布式存储情况下,业务层看到连续存储,实际上是分布在不同的服务器的不同硬盘中,数据越是分散,系统性能越高。而ROW把原数据打散之后,对性能反而有好处。
因此,整体情况下ROW基本上是对读写性能影响较小,因此是业界发展方向。

下图是几个厂家的快照实现方式:
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》
《存储引擎——Hash存储引擎,B树存储引擎,LSM树存储引擎》

目前分布式存储比较流行,华为有个FusionStorage数据,说是无限次快照,而且0性能下降,vSAN快照10%~30%的性能下降。从上面的分析来看,就知道两个场景的实现方法了。 


快照另外一个非常重要的特性是快照一致性组(Consistency Group),这个功能就是支持多个LUN或者叫卷volume同时做快照,保证数据的一致性。
如果采用阵列的快照来做数据库的备份,必须所有的LUN都是一个时间点的才行,这样数据库恢复的时候才能起来,否则数据库必须回滚到某一个一致的时间点,意味数据的丢失。比较完美的做法就是在主 机安装一个快照的agent,最好是多路径软件具备这个功能,在高端存储要做快照的时候,对主机的快照agent说,别动, 要照相了。主机agent接受到摄影师的命令后,把ORACEL主机缓存的内容flush一下到陈列来,然后hold住,阵列也尽快把cache的内容 flush到硬盘里,ORACLE用到的所有硬盘一块喊”茄子“,摄像师一按快门,一幅完美的快照就产生了。
一致性组除了保证照相的时候一致性外,还有恢复的时候要一致性恢复。这块的实现的重要性就不如照相的时候重要,可以人工选择同一时间的LUN快照恢复就可以了。最重要的是照相的时候必须要一致,而且这个人工干不了。

    原文作者:B树
    原文地址: https://blog.csdn.net/A_zhenzhen/article/details/78831866
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞