MVCC机制原理和技术细节

本次介绍MVCC。MVCC,Multi-version concurrency control,多版本并发控制。希望你了解事务、ACID特性,以及事务级别这些前置知识。

以下的介绍来自 An Empirical Evaluation of In-Memory Multi-Version Concurrency Control 这片论文。

1、基本介绍

MVCC的基本观点示DBMS对于数据库中的每个逻辑对象维护多个物理版本,从而允许在同一个对象多个操作并行。这个对象一般是元组(tuple)。多版本控制允许只读的事务获取到元组的老版本并且没有阻止写事务同时产生新版本。核心理念就是,读不阻塞写,写不阻塞读。

对,MVCC核心理念就是这么简单。

接下来我们将从事务管理设计关键理念开始介绍,1)并发控制协议 2)版本存储 3)垃圾回收 4)索引管理

介绍一下,数据库的元数据

Transactions:数据库在事务开始时给事务分配一个唯一的事务ID,事务ID是一个自然增长时间戳作为标识符(Tid)。并发控制协议使用Tid标记事务获取元组的版本。

在这里插入图片描述

Tuples:tuple每个物理版本头部有4个元数据字,借助这四个字段就可以协调事务的并发。

txn-id字段可以作为这个元组的写锁来用。例如事务T想要更新这个tuple,需要查看字段txn-id是否等于0(默认是0),如果是的话,再将自己Tid填到这个字段。这个比较和交换是借用Cas技术来保证原子性的,所以也推荐将txn-id设置到64bit。其他事务再想如此操作,就会发现字段txn-id不是0也不是自己Tid,就会知道已有一个事务占据这个元组的写锁,自己不能再写了。

字段 begin-ts和end-ts的时间戳代表这个元组版本的生命周期。这个后面我们在介绍各种并发控制协议时会详细说明。他们的初始值会设置为0。当这个元组被删除了,那么begin-ts会被设置上INF。

字段pointer存储元组上一个版本或下一个版本的地址,借助pointer就可以找到对应的版本。

废话不多说,开始正式介绍

2、并发控制协议 CONCURRENCY CONTROL PROTOCOL

2.1 TimeStamp Ordering (MVTO)

多版本时间戳排序算法(按照字面翻译的名字,简称MVTO)。其核心措施是使用事务标识符(Tid)来提前计算出序列化顺序。

在这里插入图片描述

元组(tuple)的头部还有一个字段 read_ts,用来记录上一个读这个元组的事务ID。

读操作

当事务T要读“逻辑”元组A,那么需要从多个物理元组A中找到符合要求的那一条,(1)即事务Tid在对应物理元组的begin-ts和end-ts之间。如图所示,事务T可以要读到Ax,满足10 < Tid < 20,(2)还需要确保Ax的写锁没被其他事务占住(即txn-id是0)。MVTO不允许事务读到没有提交的元组版本。此外当Tid小于 read_ts时,更新该字段。

写操作

在MVTO,事务更新的是最新版本的元组。事务T创建一个新版本Bx+1,需要满足一下(1)没有active的事务持有Bx的写锁。(2)Tid要大于Bx的read-ts。满足条件后,DBMS创建一个新的版本Bx+1,txn-id字段设置为Tid,当事务T提交时,对于Bx+1:begin-ts设置为Tid,end-ts设置为INF。对于Bx:end-ts设置为Tid。再将两者txn-id设置为0(尽管论文中没有直接提这一点,从逻辑分析上看是需要做的,否则影响后续的事务进行读)

2.2 Optimistic Concurrency Control (MVOCC)

接下来介绍多版本乐观并发控制。基于OCC的思想,DBMS认为事务大概率不会冲突,因此一个事务在读或更新元组时,不需要获取元组的锁。更重要的是DBMS也不需要为事务提供单独的存储空间,因为元组的版本信息已经预防事务去读或更新不可见的版本。

在这里插入图片描述

MVOCC协议将事务分成三个阶段。1)事务开始的时候,属于读阶段(read phase),在这个阶段事务可以进行读和更新的操作,如同MVTO,对于元组A进行读操作时,也是根据begin-ts和end-ts找到可见版本Ax,不同的是写操作,在没有获取写锁的情况下,事务T允许更改元组的版本。例如事务T要更改元组Bx,DBMS直接创建新版本Bx+1,txn-id设置为Tid。2)当事务决定提交时,事务来到了验证阶段(validation phase)。首先DBMS给事务另一个时间戳(Tcommit)从而决定事务的序列化。DBMS决定事务在read set里的元组是否被已提交的其他事务更新过。3)如果事务通过这些检查,那么将来到写阶段(write phase)。DBMS将生效所有新版本,并将begin-ts设置为Tcommit,end-ts设置为INF。

事务不应该读到一个其他未提交事务创建的新版本的元组。在MVOCC中,只有在验证阶段,事务才能发现这一情况从而中止。

2.3 Two-phase Locking(MV2PL)

多版本的两阶段锁,MV2PL使用两阶段锁方法(two-phase locking)去保证事务的序列化。对于逻辑元组的当前版本,每个事务在读或者更新之前需要获取合适的锁。锁可以不和元组存储在一起,从而不必写到磁盘。当然锁也可以放在元组头部。元组的写锁就是txn-id字段,对于读锁,read-cnt字段记录正在读该元组的活跃事务的数量。

在这里插入图片描述

想要读元组A,和上述协议一样要找到一个可见的版本,当找到可见版本时,将元组的read-cnt的值增加1,当然需要在txn-id等于0的情况下。也就是没有事务获得写锁。同样的一个事务想要更新Bx时,需要确认Bx的txn-id和read-cnt字段为0。当事务提交时,DBMS会分配一个独特时间戳(Tcommit)来更新新版本的begin-ts字段,并释放事务持有的所有的锁。

2.4 Serialization Certifier

序列化证明者。在该协议下,DBMS维护一个序列化图用于检测和移除由并发的事务形成“危险的结构”。

第一个被提出的证明者技术是系列化快照隔离技术(SSI, serializable snapshot isolation)。是的,就是PostgreSQL用的SSI。为了确保序列化,当事务创建元组的新版本,其他事务在读之前的老版本时,DBMS记录“反依赖”(anti-dependency )的边在内部的图。DBMS给每个事务维护标签且记录反依赖的边的入度和出度,当事务检测事务之间的两个连续的边,就会将这些事务中止。

个人理解,这边应该是指事务之间产生了冲突,无法确认两个事务谁先谁后,只能中止。

3、版本存储

3.1 Append-only Storage 只追加存储

在这个存储模式下,表内所有版本的元组都存在同一个存储空间。Postgres就是使用这个方法。为了更新存在的元组,DBMS首先从表请求一个空闲的槽位为了新元组版本。然后将当前版本的内容拷贝到新版本。最后再将修改生效到新分配的版本槽位。

对于追加的存储模式关键是DBMS如何组织元组版本链的顺序。因为维持一个latch-free(无锁)的双向链表是不太可能的,所以版本链只能有一个方向。

Oldest-to-Newest(O2N)旧到新

在O2N的顺序下,版本链的头是元组最老的版本。虽然这个版本可能对所有活跃事务都不见,只是因为DBMS还没有回收这个元组。

O2N的优势在于DBMS不需要更新索引,让索引指向最新版本,当元组被更新的情况下。

但是O2N需要查询时,DMBS需要遍历长的版本链从而找到最新版本,指针来不断寻找也很慢,并且读到了不需要的版本浪费了CPU缓存。因此想要在O2N有好性能依赖与系统删除老版本元组的能力。

Newest-to-Oldest(N2O)新到旧

在N2O的顺序下,在链的头部存储元组的最新版本。因为绝大多数事务需要都是元组的最新版本,所以不需要遍历版本链。缺点是,当元组出现修改时版本链的头部需要更新,相应的表的索引(包含主键索引、二级索引)都需要更新。

3.2 Time-Travel Storage 多个版本存储

直译时间穿越存储,有点怪怪的,大家有啥好建议吗?

该存储方式下,老版本的数据存在另一个表空间。DBMS对于一个元组,在主表(main table)维护一个主版本,在另一个时间穿越表(time-travel table)维护多个版本。在有些数据库中,主版本是最新版本,例如SQL Server;也有其他数据库,例如SAP HANA,主版本存的示最旧的版本。主版本存最旧的版本会带来了GC(垃圾回收)过程中额外的维护成本,当主表的版本被删除时,DBMS需要从time-travel表的数据拷回到主表。

当想要更新一个元组时,DBMS首先在time-travel表中请求一个槽位并且将主版本拷贝到该位置。索引不会因为版本链的变化收到影响,因为索引指向主版本,此时主版本没有变化。因此,这种方式避免维护数据库索引的负担,无论何时事务更新了元组。同时对于获取元组的当前版本的查询也是很理想。

3.4 Delta Storage 增量存储

delta在数学术语中,有增量,变动量的含义。

在该模式下,DBMS在主表维护元组的主版本在另一个delta存储空间存储delta版本。delta存储在MySQL和Oracle称之为回滚段(rollback segment)。多数DBMS都是选择在主表中存储当前版本(也就是最新版本)。当更新一个元组时,DBMS会从delta存储空间申请一个连续空间用于创建一个新的delta版本。该delta版本只会记录更改的元素的原先值,而不会记录整个元组(这一点有别于 time-travle storage)。然后DBMS直接就地更新主表上主版本的内容。这种模式对于更新操作很理想,因为减少了内存的分配。但是对于偏向与读的场景负担大。当进行获取一个元组的多个属性的读操作时DBMS需要遍历版本链,从而为每个元素找到对应的版本。

4 GARBAGE COLLECTION 垃圾回收

垃圾回收的流程分成3个步骤:(1)检测出失效的版本,(2)从版本链以及索引进行删除,(3)回收存储空间。什么是失效版本,要么这个版本是一个无效版本(例如被一个abort的事务创建出来),要不该版本已经对任何活跃的事务处于不可见状态。对于后者,DBMS会检查版本的end-ts小于所有活跃事务的Tid才是。

当前GC的实现主要有2种。第一种元组层面(tuple-level)的GC。DBMS直接检查元组的可见性。第二种是事务层面的GC检查一个完成的事务创建的所有版本是否可见。

4.1Tuple-level Garbage Collection 元组级别的垃圾回收

后台线程回收 Background Vacuuming (VAC)

DMBS使用后台线程来定期扫描数据库。这个方式是容易实现的也是常见。但是这个机制对于大的数据库来说不好扩展,尤其GC的线程不多的情况下。有一种利于扩展的方式事务将无效的版本注册到 latch-free 的数据机构上,GC线程后面回收这些过期版本,使用epoch-based方式。还有一种优化是DBMS维护dirty block (脏的块)的位图,这样回收线程不需要检查那些block,上次回收已经检查过之后也没有修改的block。

Cooperative Cleaning 协同清理

当执行一个事务时,DBMS会遍历版本链来找到可见的版本。在遍历过程中,DBMS会分辨出过期版本并将它们记录到全局的数据结构上。这个方式便于扩展,这样一来回收线程无需检测过期版本,但这个方式比较适用于O2N的存储方式。另一个挑战时,如果事务没有访问特定的元组的版本链,那么永远不会移除这些过期版本。这个问题在Heckton也被称之为“布满灰尘的角落”。但是DBMS也可以用一个单独的线程来进行完全的GC检查从而解决这个问题。

4.2 Transaction-level Garbage Collection 事务级别的垃圾回收

在事务的粒度回收存储空间。DBMS认为一个事务已经过期,当其产生的元组不被任何其他事务可见。当一次epoch结束,所有该事务产生的元组可以被移除。相较于元组级别,这种方式更加简单因为DBMS可以立即回收对应事务的所有存储空间。当然也有缺点,缺点就是DBMS需要追踪事务在每次epoch中读和写的元组集合。

5.索引管理

对于索引如何保存版本信息,也是数据库MVCC的主要区别。对于索引项,也就是键值对(key/value)。键(key)是元组用于索引的属性,值(value)是指向元组的指针。

主键索引的索引项中的值指向元组的当前版本,但是DBMS更新主键索引的频繁程度取决于元组是否被更新,当存储引擎创建一个新版本时。例如在delta模式下,指向元祖的主版本在主表上,所以索引无需更新。对于append-only模式下,取决于版本链的顺序,例如N2O的顺序下,当新版本元组创建时,主键的索引项的值也会更新。如果主键被更改,DBMS会在索引上进行删除再添加。

对于非主键索引,索引项的键和值都会改变。当前在索引项上的值有两种记录方式,第一是逻辑指针,使用间接映射到物理位置,第二种就是直接使用物理位置

5.1 逻辑指针

使用逻辑指针的主要思想就是使用一个固定指针,索引项中指向的内容不变。正如该图所显示,DBMS使用一个间接层将元组的标识符(元组的id)映射到版本链的头。但因为没有指向确切的版本,DBMS需要遍历版本链找到可见的版本。

在这里插入图片描述

目前有2种方式来实现逻辑指针。

Primary Key(PKey):元组的标识符使用元组的主键。当DBMS从非主键索引获取到对应索引项,然后根据主键的索引项找到版本链的头。

Tuple Id(TupleId):Pkey指针的一个缺点是,数据的存储负荷随着元组的主键大小增加而增加,因为每个非主键索引有一个完整的副本。除此之外,因为多数DBMS使用一个保证顺序的数据结构存储主键索引,查找的代价依赖于索引项的数目。一个替代方案就是使用一个64-bit元组标识符代替主键,再用一个无锁的Hash表维护映射关系,将元组标识符映射元组版本链头

5.2 物理指针

在这里插入图片描述

DBMS直接在索引项上存储版本的物理地址。该方法只适合追加的版本存储,因为DBMS存储相同表中的版本,因此所有的索引项可以指向他们。当更新在表中的任何tuple时,DBMS插入最新的版本到所有的非主键索引。在这种方式下,DBMS查找元组从非主键索引而不用比较索引键值和所有的版本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值