白话数据库中的MVCC

Posted ImportSource

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了白话数据库中的MVCC相关的知识,希望对你有一定的参考价值。

说MVCC(Multiversion concurrency control,多版本并发控制)之前,先从数据库的ACID说起。ACID其中一个就是I。也就是Isolation,隔离性。


ACID中的I


数据库的隔离性是一个非常重要的概念。隔离主要隔离的是事务,一个事务要和其他事务隔离。


所以,一看到隔离,你要知道隔离的真正含义。


事务和事务之间是隔离的


事务之间要隔离到什么程度,是有统一的规定的,这个规定就是SQL标准。在SQL-92之后,就新加了对隔离级别的定义。


白话数据库中的MVCC


1、隔离级别


共有四个级别:

  • Read uncommitted(读取未提交)

  • Read committed(读取已提交)

  • Repeatable read(可重复读)

  • Serializable (串行)


这四个级别逐步加重。到最后的Serializable,事务和事务之间彻底什么都看不到,而且每次事务执行还要把其他事务全部卡起,全部串行。


详细解释:


Read uncommitted 一个事务可以看到到(读取)其他事务还没有commit的数据(会引起“脏读”)。


Read committed 一个事务可以只能看到(读取)其他事务commit了的数据。


Repeatable read 一个事务重复读取一条数据都能读取到相同的数据。但无法解决“幻读”问题。


Serializable  一个事务在执行期间,彻底排他。比如a事务执行了SELECT * FROM tb_crm_order,此时a事务会把tb_crm_order表的所有数据全部锁定,而不允许任何其他事务进行insert或update。这个级别强调的是对“数据的范围(range)”的排他锁定。只会锁定查询范围内的数据。比如 SELECT * FROM tb_crm_order WHERE status='CLOSED' ,那么只会锁定状态为'CLOSED'的数据。


2、三个问题


简单的介绍了四个级别后,也许你对这四个级别中的某些级别还是没有一个清楚的认识。要想把四个级别彻底理清楚,还需要准确理解三个概念。脏读(dirty reads)、不可重复读(Un-Repeatable read)、幻读(Phantoms)。


  • 脏读(dirty reads)


a事务读取到了b事务还没有commit的数据。


  • 不可重复读(Un-Repeatable read)


a事务在整个事务内多次读取id=12的数据,总是能读到一致的数据,不会出现不一致性的情况,比如第一次读取的时候name='魏璎珞',第二次的时候却读到name=‘吴谨言’,如果读到不一样就认为是不可重复读,Un-Repeatable read。这里要注意的是,可重复读面向的具体的某一条数据的前后一致性。


  • 幻读(Phantoms)


幻读则强调的是在一个事务内,两次读取到了不一样的数据集。比如,同样的sql查询条件的sql,第一次读取到了3条数据,第二次却读到了4条数据,第三次却没读到。幻读面向的是 数据集,也就是range。而Repeatable read(可重复读)面向的是指定某条数据的前后一致性。


这三个概念理清之后,你基本上也就分清了四个隔离级别了。


   表3 四个隔离级别对应的的问题所在


可重复读(Repeatable read)是mysql innodb的默认隔离级别。通过表3我们发现可重复读虽然没有了脏读和不可重复读的问题,但依然存在幻读的问题。既然是个问题,那就得解决,毕竟默认的隔离级别就是可重复读,只有把问题解决才能更好的对外服务。


他的解决思路就是通过MVCC(多版本并发控制,Multiversion concurrency control)来解决幻读问题。这里再重申一遍,幻读就是指同一事务内多次执行相同查询条件的查询sql有可能获取到不同的数据集合。


现在需要解决的问题就是让同一个事务内多次执行同一查询sql都能获取到相同的数据集合。


为什么要用MVCC?


并发控制可以通过加锁的方式来做,但加锁无疑性能堪忧,显然这是不合适的,于是有人就想出来其他的不加锁的方式,比如MVCC这种“乐观锁”的方式。


MVCC可以解决哪些问题?


通过前面的的讲解,我们知道在四个隔离级别中,第三个级别也就是可重复读级别,需要解决幻读的问题。所以MVCC可以帮我们解决幻读的问题,除此还可以解决不可重复读的问题。


日常开发中的例子和一些思路


为了更好的理解MVCC,在正式介绍MVCC之前,让我们先回到日常的开发工作中,来看看日常的例子,看会不会有所启发。


  • 例子(思路)1:Compare And Set


比如,我通过web修改一个数据,我读取到数据表单后,然后我出去吃饭了,等到吃完饭回来再提交。


如果是通过纯粹加锁的机制,那么此时此刻,其他人就无法修改这条数据了,因为我出去吃饭了,一直锁着这条数据。


这在现实中显然是不可接受的。


理想的情况就是我可以在拉取的时候记录下我拉取的时间,然后我提交的时候再通过和数据库的更新时间作比对,如果和数据库的当初记录的时间不一致了,那么就认为是冲突了,此时就更新失败。如果是一致的,那么认为在这段时间内没有其他人更新,则更新成功。


通过记录时间戳的方式就实现了并发的控制。


  • 例子(思路)2 :操作日志法


再来一种做法。


比如,我直接通过log的方式来对数据进行操作,每次操作都入库,然后携带上时间戳(ts,timestamp)。


比如,a用户对id为1的数据修改了,然后b用户也对此数据进行修改了,这些数据我都记录下来,最后针对一条数据的修改会有很多条log数据。


最后通过垃圾回收的方式,把那些老旧的log数据删除掉,只保留最新的一次修改。


这样也是通过timestamp时间戳实现了并发控制。


  • 例子(思路)3 :快照法(类似Copy-On-Write)


再来一个例子。


比如,我每次编辑一条数据,我就在库里保存一条该数据的瞬时快照。然后针对这个快照进行更新操作。其他线程读取的时候依然去读取旧的原始数据,实现了读取和写入的分离,数据达到最终一致性。写入的时候再加锁。但这种场景适合读多写少的场景。


  • 例子(思路)4 :HTTP中的ETag和if-match


还有一个http的例子。


比如HTTP中,由于HTTP是无状态的,所以你无法加锁,只能使用乐观锁机制,HTTP的GET方法返回资源时,会设置一个ETag在headers里面,后续的PUT方法更新资源时,就需要通过if-match匹配ETag。由于ETag是基于第一个GET的资源产生的,所以只会匹配第一个GET。 


  • 例子(思路)5 :java.util.concurrent.atomic的CAS


Java的java.util.concurrent.atomic使用CAS(Compare And Set(或Compare And Swap)算法实现。此处不再赘述。


通过上面的各种做法,你会发现,版本号或者时间戳是一个非常有用的概念。


没错,版本号或时间戳很有用!


MVCC两种实现方式


纵览各种数据库的MVCC实现,主要有两种实现方式。第一种方式就是通过保存多个版本的数据,然后通过gc的方式清理那些不再被使用的数据。比如,PostgreSQL、Firebird、Interbase就是采用这种方式。SQL Server也采用类似的方式,略微不同的是,SQL Server把老版本的数据保存在了tempdb数据库中(一个有别于主数据库的数据库)中。第二种方式是通过数据结合undo log的方式。这种方式只会保存最新版本的那一份数据,然后通过undo log来进行重新构造需要的老版本数据。采用这种方式的数据库有Oracle 、MySQL(Innodb)。


  • 核心思路


MVCC主要解决的就是不可重复读和幻读问题。其实核心就是通过设置类似事务ID(作为一种版本号格式吧)的方式来解决的。假设我们给每条数据后都增加两个字段,一个是“新增时间戳 its(表示insert timestamp)”,一个是“删除时间戳dts(delete timestamp)”。这里我们假定维护一个全局事务ID生成器,事务ID是随着时间的推移递增的。每次事务执行,事务ID都会加1(比照时间戳更容易理解)。


新增


只要新增了数据,我就把执行新增的事务的事务ID写入到its。


更新


更新数据,通过新增一条新数据和删除老数据两步来实现。新增新数据时同样把更新所在的事务的事务ID写入到its字段中,删除老数据则只是把老数据的dts字段设置为当前更新事务的事务ID即可(逻辑删除)。


删除


删除数据,同上,只要把要删除数据的dts设置为当前事务的ID即可。


这样的话,在同一个事务(假如是事务a,事务ID为20)中,进行读取数据的时候,就只需要读取its小于20且dts为空的数据。这样就可以实现可重复读。


并且也解决了幻读的问题。因为你通过事务ID的方式把数据给卡在了事务开启前,之后所开始的其他事务的事务ID都要大于20。这样我们就无法获取到其他事务新增或删除的数据行了。


ps:以上我们只是通过事务ID来说明问题,其实mysql InnoDB的内部实现是通过系统版本号来进行的。系统版本号每次新开始一个事务都会加1。而且由于它是有时序的,所以你可以认为它其实就是等价于时间戳的。


  • 模拟


以下是我们模拟数据库的数据保存方式来展现mvcc的一般做法,其中显示了每行的两个隐藏字段its和dts。分别表示“插入时间戳”和“删除时间戳”。其中更新操作是通过插入一条新数据,然后删除(逻辑删除,只是把dts设置为当前事务的事务ID(或当前事务所用的系统版本号))老数据的方式来进行的。


insert


id  name                    its  dts

1  name  其他信息     1


新增时  把当前的时间戳写入its(表示insert timestamp)


update


id name                   its     dts

1 name1  其他信息    1          (新增数据)

1 name    其他信息            1  (删除数据)


delete


id name                   its       dts

1 name    其他信息   1         2   (删除数据)


通过以上的阐述相信你已经大概知道mvcc是如何运作的了。以上我们只是阐述了mvcc的基本思路。具体的指定的数据库则内部实现会略有不同。mysql的innodb引擎是通过undo log和数据两部分来控制的,类似我们上面提到的那个例子通过操作log来进行。还有比如在innodb中也支持通过间隙锁(next-key locking)来防止幻读。在某个事务中,间隙锁不仅要锁住待查询的行,同时还要对索引中的间隙进行锁定,以防止幻影行的插入。当然这个观点是从《High Performance MySQL》中得来的,而且这只是解决幻读的一种方式,严格来说与mvcc并无关系,本文我们讨论的重点只是mvcc。


总之,MVCC没有正式的规范,所以各个数据库和存储引擎的实现都不尽相同,以上的所述的MVCC实现思路是一般意义上的多版本并发控制。


 建立体系


重要的是,我们要对MVCC的认识建立一个体系,而不是只言片语的学习技术知识。下面尝试画一个图把各个点串联起来。


MVCC思维体系

以上是关于白话数据库中的MVCC的主要内容,如果未能解决你的问题,请参考以下文章

3什么是MVCC

MySQL中的MVCC(r12笔记第35天)

MySQL的MVCC

MySQL高级-MVCC(超详细整理)

MySql MVCC是如何实现的-MVCC多版本并发控制?

MVCC多版本并发控制