前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >白话数据库中的MVCC

白话数据库中的MVCC

作者头像
ImportSource
发布2018-10-23 11:23:31
1.5K0
发布2018-10-23 11:23:31
举报
文章被收录于专栏:ImportSourceImportSource

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

ACID中的I

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

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

事务和事务之间是隔离的

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

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思维体系

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档