An Empirical Evaluation of In-Memory Multi-Version Concurrency Control

文章目录

  • MVTO
  • MVOCC
  • MV2PL
  • 总结

MVTO

看名字就知道是多版本时间系列(Multi-version timestamp ordering)

这个mvcc方法不支持读未提交(commit)的tuple,也就是说一个tuple的写锁被持有那么就不允许其他事务读,更别说是写,这个严格执行序列化

还是说一个tuple的架构

| header |  column |
     |
     ↓
| txn-id | read-ts | begin-ts | end-ts|
txb-id就是说当前tuple被谁持有,持有的事务id是多少,
begin-ts和end-ts描述一个tuple的生命周期,当tuple被删除
read-ts是代表谁目前在读这个tuple

假设我们有以下的tuple

   | txn-id | read-ts | begin-ts | end-ts |
Ax | 0      |    0    | 10       | 20     |

我们有如上的tuple Ax,他的header如上,此时我们有一个事务T想读他会先干如下的几件事

  • 首先确定事务Tid位于那个时间段内(哪个begin-ts和end-ts中间)
  • 确定版本后再确定写锁是否被占有(txn-id是否为0或者为自己Tid,因为MVTO不允许读未commit的数据)
  • 读完后DBMS会set这个tuple版本的read-t为Tid,当然要这个原先的值比Tid小(如果刻意让一个事务去读一个老的版本那么老的版本read-t不会被更新)

上述MVTO的读过程,我们来简数MVTO的写过程

假设我们有以下的tuple

   | txn-id | read-ts | begin-ts | end-ts |
Ax | 0      |    0    | 10       | 20     |
Bx | 0      |    17   | 15       | 30     |

在了解写或者说更新之前,我们要知道MVTO要更就最新最后一个版本,假如事务T想更新tuple Bx,那么首先是找B的最新版本,此时我们更新最新版本为Bx+1,更新的条件如下

  • 确定写锁是否被占有(txn-id是否为0或者为自己Tid,因为MVTO不允许读未commit的数据)
  • Tid要大于Bx的read-ts

上述的条件如果满足,那么就创建一个新的版本Bx+1然后set这个新版本的txn-id为Tid,当这个事务T执行完毕(commit)后DBMS set Bx+1这个版本的begin-ts为Tid,end-ts为INF,Bx版本的end-ts为Tid

   | txn-id | read-ts | begin-ts | end-ts |
Ax | 0      |    0    | 10       | 20     |
Bx | Tid    |    17   | 15       | 30     |
Bx | Tid    |    0    | --       | --     |   //新版本的事务还在执行

如何保证并发事务线性化的呢?
假设我们有2个事务分别为读事务,和写事务,假设读事务在前r1,写事务在后w2
r1先读一个tuple,这个tuple的header的read-ts将会被设置为r1(假如没有别的事务持有写锁ANDread-ts为0)
r1读完后w2准备写这个tuple,写的时候先确定写锁是否被占有,发现写锁为0,再看读锁是否被占有,发现read-tx被占有但是read-tx比自己小那么我们可以创建一个新的version

总感觉有啥问题?经过细想后我们发现在并发环境下,每个事务到达数据库的时间是不一样的,但是因为并发的原因,他们都不是按照时间戳先后到达服务器或者说到达数据库的执行层面不是按照事务时间戳排序的,所以我们上面TO就是time order就是按照时间戳把多个并发事务按照时间戳规定好执行的顺序,谁先读的谁先写的根据时间顺序一个一个执行,我们这么多MVCC算法的目的就是为了达成一致性

如果是w1r2呢?如果写操作没有完成,那么txn-id还是存在,所以r2读事务就不能读被block住,it’s make sense

MVOCC

mvocc的全称为multi version optimistic concurrent control
有了上面MVTO的基础,我们理解MVOCC就更容易

MVOCC分为3步

  • read phase:这一步当事务准备在dbms上进行读和跟新操作,和MVTO一样一个事务的读操作到tuple上后先根据自己事务的时间戳再去tuple多个版本的begin-ts和end-ts去找,找到自己应该访问的版本,(begin-ts < 事务本身的时间戳 < end-ts),假如我们的事务是跟新操作呢?我们先去找这个tuple的写锁是否被其他的事务占有,如果没有就自己占有,并且写一个新的版本出来比如tuple Ax+1

    看似和MVTO差不多

  • validation phase:这一步相当于验证,具体怎么操作呢?DBMS会给事务一个新的时间戳(Tcommit就是最新的时间戳,已经提交更新后的时间戳),用这个时间戳去验证是否事务是序列化了,DBMS会探测是否事务读到的tuple的某个版本是被事务更新过的

    DBMS分配给事务一个新的时间戳是让事务通过这个新的时间戳去读某个特定的版本,(begin-ts < T新时间戳 < end-ts),然后DBMS要确认是否读到的数据是事务已经提交的数据

  • write phase:通过上面的validation phase的检查,就开始写,DBMS将会install 所有的new version并且设置其begin-ts为Tcommit(Tcommit就是最新的时间戳,是已经提交更新后的时间戳),end-ts为INF

第二段的validation phase看起来有一些拗口,可能你会有以下的疑问,比如说我们读的版本本来是最新版本,再给他分配一个最新的时间戳(事务已经提交commit后的时间戳)没有意义啊,其实在多个事务共同操作一个tuple的时候,update操作只能update最后一个版本,而read操作总是不能读到最新的版本,因为其他的事务还没commit,在上述的MVOCC中如果我们读到过时的时据则会abort(第二步),如果通过第二步说明读到的不是过时数据,也许另一个事务在我们read执行到第二步之前还没commit并且read操作读的是最新的version(根据事务的时间戳匹配),此时就不应该算过时数据

MV2PL

MV2PL(multi version 2 phase locling),这个MVCC的方法提出就比较的早了,早在1980年就提出,而MVOCC是2010年后才使用在数据库上的,MV2PL非常有意思

在MV2PL中tuple的格式和上面的2种方法有些不一样,他的格式如下,txn-id和其他一样都是写锁,read-cnt代表有几个读的事务在读,其他的都一样

| txn-id | read-cnt | begin-ts | end-ts|

首先我们将事务分为2个情况分别为读和写

  • 首先我们的读的第一步和上面一样根据时间戳找到对应的version
  • 如果写锁是空的(为0或者T本身)然后DBMS increment这个version的read-cnt

  • 首先确定txn-id是否为0(也就是其他的事务是否持有写锁) AND read-cnt是否为0(是否有其他的事务在读),假设都不成立就不进行写而是等待
  • 写完后分配一个新的时间戳(Tcommit),用这个新的时间戳去更新begin-ts

总结

首先我们说MV2PL,当一个事务准备读一个tuple的version,恰好其他的事务准备写这此tuple的version那么就会abort(因为这个version的read-cnt不为0),而MVTO用read-ts去代替read-cnt,注意的是MVOCC没有维护任何读的字段在header中,某一个事务读某个version就直接读,不会更改某一个header,这样避免其他的事务读/更新同一个version发生的abort(在mv2pl和mvto中也许一个时间戳大的事务先读了一个版本,后面一个时间戳小的事务准备读这个事务可能在mvto中失败,一个时间戳小的事务准备写这个事务可能在mv2pl中失败)

你可能感兴趣的:(论文,数据库,数据库)