悲观锁,乐观锁以及MVCC
在上文中,我们探讨了MySQL不同存储引擎中的各类锁,在这篇文章中我们将要讨论的是MySQL是如何实现并发控制的。并发问题有三种,分别为:
- 读-读,不存在任何问题
- 读-写,有隔离性问题,可能遇到脏读(会读到未提交的数据) ,幻影读等。
- 写-写,可能丢失更新
首先我们先来看一下悲观锁和乐观锁:
- 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
悲观锁(Pessimistic Lock)实际上是悲观并发控制(Pessimistic Concurrency Control,“PCC”),顾名思义,就是对并发问题持有悲观的态度,认为每次对数据的操作都会引发并发冲突,因此悲观锁每次操作数据的时候都会上锁,以屏蔽一切可能违反数据完整性的操作。
悲观锁实际上就是运用了上一篇中提到的各类锁来实现并发控制,较为简单,但是开销比较大,而且只支持读-读并发,即对于同一个数据来说,如果采用悲观锁,那么读-写和写-写并发是不被允许的。
- 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁(Optimistic Lock)实际上是乐观并发控制(Optimistic Concurrency Control,“OCC”),对并发问题持有乐观的态度,认为不会发生并发冲突,因此不会上锁(所以乐观锁并不是一种锁)。但是,在提交更新的时候会判断一下在事务期间是否有其他进程更新了同一块数据。乐观锁解决了写-写冲突的无锁并发控制(注意,这边的无锁并不是真正的无锁,而是在执行过程中不加锁,在检测是否冲突的时候还是需要对数据进行加锁,但是这边加锁的时间明显少了很多)。乐观锁一般来说有以下两种实现方式:
1.使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:
如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。
2.使用时间戳(timestamp)。乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
- 悲观锁和乐观锁的应用场景
从上文可以看到:
1. 当发生并发冲突的概率比较大时,悲观锁更合适,以提高事务的成功率。
2. 当发生并发冲突的概率小时(如读多写少),乐观锁更合适,可以提高系统的吞吐量。
接下来我们来看看MVCC是什么。
MVCC的意思为多版本并发控制(Multiversion concurrency control),它解决的是读-写并发的问题。MVCC一般来说也可以看成是一种乐观机制,和间隙锁一样,它可以用来解决幻读的问题,只是间隙锁解决幻读是用使写进程阻塞的方式来进行的,而MVCC是以快照的方式来处理这一问题。不同数据库版本对MVCC的实现机制不同,在这边我们讨论InnoDB是如何进行MVCC的。
InnoDb 会为每一行记录增加两个字段,当前行创建时的版本号和删除时的版本号(可以为空),事务在写一条记录时会将其拷贝一份生成这条记录的一个原始拷贝,写操作同样还是会对原记录加锁,但是读操作会读取未加锁的新记录,这就保证了读写并行。MVCC具体操作如下:
SELECT:InnoDB会根据以下两个条件检查每行记录:
1)InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
2)行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
INSERT:InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE:InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE:InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当系统的版本号为原来的行作为删除标识。
InnoDb 通过 MVCC 实现了读写并行,但是在不同的隔离级别下,读的方式也是有所区别的。首先要特别指出的是,在 read uncommit 隔离级别下,每次都是读取最新版本的数据行,所以不能用 MVCC 的多版本,而 serializable 隔离级别每次读取操作都会为记录加上读锁,也和 MVCC 不兼容,所以只有 RC 和 RR 这两个隔离级别才有 MVCC。
尽管 RR 和 RC 隔离级别都实现了 MVCC 来满足读写并行,但是读的实现方式是不一样的:RC 总是读取记录的最新版本,如果该记录被锁住,则读取该记录最新的一次快照,而 RR 是读取该记录事务开始时的那个版本。虽然这两种读取方式不一样,但是它们读取的都是快照数据,并不会被写操作阻塞,所以这种读操作称为 快照读(Snapshot Read)。快照读在InnoBD的实现中就是普通不加锁的select语句。与快照读相对应的是当前读,即处理的都是当前的数据,需要加锁,如select ... lock in share mode,for update以及select,update和delete,在解决当前读的幻读问题时,MySQL使用了间隙锁的机制。
参考文档:
https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/
http://www.cnblogs.com/chenpingzhao/p/5065316.html
https://riverdba.github.io/2017/04/01/MVCC-theory-study/