白话数据库中的MVCC

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

原文发布于微信公众号 - ImportSource(importsource)

原文发表时间:2018-09-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏云计算教程系列

如何使用MySQLTuner优化MySQL性能

MySQLTuner是一个用Perl编写的脚本,帮助你提高MySQL性能及稳定性。它通过检索当前配置变量和状态数据,提供一些基本性能建议。

20550
来自专栏芋道源码1024

事务隔离级别和脏读的快速入门

仅从ACID或非ACID角度考虑问题是不够的,你应知道你的数据库支持何种事务隔离级别。

7610
来自专栏云计算教程系列

如何使用Symlink更改MySQL数据目录

数据库随着时间的推移而增长,有时会超出文件系统的空间。当它们与操作系统的其余部分位于同一分区时,可能会遇到I/O冲突。RAID,网络块存储和其他设备可以提供冗余...

18460
来自专栏小怪聊职场

MySQL(六)|《千万级大数据查询优化》第二篇:查询性能优化(2)

32190
来自专栏CaiRui

Zabbix监控详解

Zabbix是什么 Zabbix 是由Alexei Vladishev创建,目前由Zabbix SIA在持续开发和支持。 Zabbix 是一个企业级的分布式开源...

2.2K80
来自专栏沃趣科技

ASM 翻译系列第二十一弹:ASM Attributes Directory

原作者:Bane Radulovic 译者: 郭旭瑞 审核: 魏兴华 DBGeeK社群联合出品 ASM Attributes Directory A...

33940
来自专栏杨建荣的学习笔记

由小见大-MySQL脚本部署中的一些策略

在线上环境中部署脚本,可谓是常在河边走,哪有不湿鞋,所以大大小小的案例总结下来,还是会发现一些有趣的地方,这些可以作为操作时的一些参考,仅供参考而已。 第一类...

34860
来自专栏容器云生态

Docker监控方案(TIG)的研究与实践之Influxdb

前言: Influxdb也是有influxdata公司(www.influxdata.com )开发的用于数据存储的时间序列数据库.可用于数据的时间排列。在整个...

32880
来自专栏码神联盟

数据库 | MYSQL 中的视图view详解

序本文目录 什么是视图 视图的特性 视图的作用 视图使用场景 视图示例1-创建、查询 视图示例2-增、删、改 其它 1什么是视图 视图是一个虚拟表,其内容由查询...

464110
来自专栏杨建荣的学习笔记

关于视图和存储过程的权限问题探究 (r9笔记第87天)

今天在处理一个工单的时候发现了一个奇怪的现象,开发同学需要创建一个存储过程,目前的架构类似这样的形式 ? 数据库中存在一个属主用户,表,存储过程等对象...

361100

扫码关注云+社区

领取腾讯云代金券