MySQL的事务机制和锁(InnoDB引擎、MVCC多版本并发控制技术)
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | Life_Goes_On
来源 | urlify.cn/qmmYry
一、事务(数据库的事务都通用的定义)
1.1 事务定义
事务是由一步或几步数据库操作序列组成逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。事务通常以 BEGIN TRANSACTION
开始,以COMMIT
或 ROLLBACK
操作结束:
COMMIT
即提交,提交事务中所有的操作、事务正常结束;ROLLBACK
即回滚,撤销已做的所有操作,回滚到事务开始的状态。
1.2 事务的四种特性
ACID:原子性,一致性,隔离性,持久性。
原子性(Atomicity) | 指事物在逻辑上是不可分割的操作单元,所有语句要么都执行,要么都撤销执行。 |
一致性(Consistent) | 一个事务本质是将数据从一种一致性状态转换到另一种一致性状态,具体取决于现实生活的逻辑。(比如转账,A转给B,操作前后A+B的钱是不变的) |
隔离性(Isolation) | 隔离性是针对并发事务而言的,同时处理多个事务的时候,数据库的事务提供了不同的隔离级别来保证正确。 |
持久性(Durable) | 事务一旦提交,对于数据的修改是持久性的,数据更新的结果已经从内存转存到外部存储器,即使系统故障,已提交的数据更新也不会丢失。 |
这四个特性在没有并发的时候显然很容易满足,但是在并发处理事务的情况下,可能会带来一些问题
丢失更新(Lost Update) | 当两个或多个事务操作同一行,后面的事务修改的值会覆盖前面的事务修改的值。 |
脏读(Dirty Reads) | 一个事务读到了被另一个事务修改,但尚未提交的事务。当一个事务正在多次修改一个数据,而这一系列修改还没有最后提交,另一个并发事务来读取了,就会导致错误。也就是另一个事务读到了脏数据。 |
不可重复读(Non-Repeatable Reads) | 一个事务操作的过程里,先读取一个数据,后来又读取,而两次读出的数据值不一致。就是因为中间被别的事务修改了。 |
幻读(Phantom Reads) | 一个事务按照相同的查询条件查两次,第一次查出了A集合,第二次却不是了,因为其他事务插入了数据,正好满足这个事务的查询条件。 |
注意事项:
脏读
和不可重复读
的区别:脏读读到的脏数据是另一个事务没有提交的数据,但是不可重复读读到错误数据是因为另一个事务把数据修改并提交了;幻读
和不可重复读
的区别:幻读和不可重复读都是读到了另一个事务提交的数据,但是不可重复读是两个事务针对同一个数据项,而幻读针对的是一个数据整体(数据条目)
为了解决上述提到的事务并发问题,数据库提供一定的事务隔离机制来解决这个问题。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使用事务在一定程度上“串行化” 进行,这显然与“并发” 是矛盾的。
RU:READ UNCOMMITED | 未提交读(很少使用,基本没有解决问题) | 丢失更新 |
RC:READ COMMITED | 提交读。顾名思义,保证一个事务只能看见另一个事务已经提交的事务的结果。 | 丢失更新+脏读 |
RR:REPEATEABLE READ(Innodb默认) | 可重复读。顾名思义,解决了第三个并发问题:不可重复读。 | 丢失更新+脏读+不可重复读 |
S:SERIALIZABLE | 序列化。通过强制事务排序来让他们串行执行。(也很少使用)本质上是给每个数据行都加上了共享锁 | 四个问题都解决了 |
从上往下,隔离级别越来越高,但是代价肯定越来越大,真正选择的时候需要斟酌,可以看到,要想真正解决幻读问题,需要隔离级别为 S。
注意:
事实上 Mysql 的 InnoDB 通过 MVCC (Multi-Version Concurrent Control,多版本并发控制)机制解决了不可重复读的基础上,又解决了幻读的的问题。
二、MySQL的锁(结合 InnoDB引擎)
2.1 背景
对于数据库事务的并发控制技术有很多,基于锁、基于时间戳、基于MVCC的并发控制、基于MVCC的可串行化快照隔离等。
而我们讨论的概念,是MySQL的事务,再具体一些,是 InnoDB 支持的事务。
InnoDB
是支持 ACID
的,而MySQL用 InnoDB
作为自己的默认存储引擎,事务管理是 MySQL Server
实现框架和接口定义,而 InnoDB
提供具体的事务操作和并发控制,所以 MySQL 的事务模型
,主要是指 MySQL 的InnoDB
的事务管理部分。
InnoDB 使用锁和 MVCC 技术来实现并发事务的访问控制技术。
其中,锁是并发控制的基础,在此基础之上,实现了 MVCC 机制,用以提高基于锁的方式带来的低效率问题。
有了这个概念,我们下面分别讨论 锁 和 MVCC 两个内容。
MyISAM 默认使用的是表锁;
InnoDB 支持行级锁,加上 MySQL Server 支持表级锁,所以结合事务的时候,使用 InnoDB 引擎,系统就默认使用的是行级锁。
里我们讨论的锁的实现,是为了事务的并发控制,所以都是在使用 InnoDB 引擎下的情况,那么有一些概念可能在其他引擎下的实现也是类似的。
2.2 锁的分类
从对数据操作的粒度分 :
表锁:操作时,会锁定整个表。(
MySQL Server
)行锁:操作时,会锁定当前操作行。(也叫
Record Lock
,记录锁
),实际上他是在索引上的记录之锁,因为 InnoDB 的表的组织结构是通过 B+ 树索引。
从对数据操作的类型分:
读锁(共享锁、S锁):针对同一份数据,多个读操作可以同时进行而不会互相影响。
写锁(排它锁、X锁):当前操作没有完成之前,它会阻断其他写锁和读锁。
意向共享锁(IS):事务想要获取一张表中某几行的共享锁
意向排它锁(IX):事务想要获取一张表中的某几行的排它锁
前两个很容易理解,他们的兼容情况是:只有 S 锁和 S 锁是兼容的,其他的组合都是互斥的。
因为锁的粒度不同,这就允许事务表级和行级的锁可以同时存在,所以 InnoDB 支持了额外的一种锁叫,意向锁(Intention Lock
):
问题:innodb的意向锁有什么作用?
mysql官网上对于意向锁的解释中有这么一句话
“The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.”
意思是说加意向锁的目的是为了表明某个事务正在锁定一行或者将要锁定一行。
那么,意向锁的作用就是“表明”加锁的意图,可是为什么要表明这个意图呢?
如果仅仅锁定一行仅仅需要加一个锁,那么就直接加锁就好了,这里要表明加锁意图的原因是因为要锁定一行不仅仅是要加一个锁,而是要做一系列操作吗?
①在mysql中有表锁,LOCK TABLE my_tabl_name READ; 用读锁锁表,会阻塞其他事务修改表数据。LOCK
TABLE my_table_name WRITe; 用写锁锁表,会阻塞其他事务读和写。
②Innodb引擎又支持行锁,行锁分为共享锁,一个事务对一行的共享只读锁。排它锁,一个事务对一行的排他读写锁。
③这两中类型的锁共存的问题考虑这个例子:
事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。
数据库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。
数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表
step2:判断表中的每一行是否已被行锁锁住。
注意step2,这样的判断方法效率实在不高,因为需要遍历整个表。
于是就有了意向锁。在意向锁存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下, 上面的判断可以改成
step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。
注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
总结:为了实现多粒度锁机制(白话:为了表锁和行锁都能用)
那么这四个锁在一起之后我们看看他们的兼容性:
2.3 其他锁
InnoDB里还有几个锁:
间隙锁(
Gap Locks
):两个索引项之间的间隔、称为间隙,把这个间隙看作一个对象,在此对象上加锁,就是间隙锁。这个锁是从加锁的对象角度定义的锁,所以和表、行是同一个角度的锁。Next-Key锁(
Next-Key Locks
):行级锁+间隙锁共同组成。Insert Intention Locks:基于间隙锁,专门用于 Insert 操作。
间隙锁会在 RC 隔离级别的某些情况下使用,在 RR 隔离级别下,间隙锁会和行级锁合并成 Next-key 锁使用。(记住这一点)
2.4 锁的施加细则
对于各种 MySQL 语句来说,InnoDB 对他们提供了事务操作的支持,这样的支持就是通过并发控制的锁来完成的。(当然,还有补充的 MVCC 技术,后面再说)
细则如下,(参考《数据库事务处理的艺术》这本书里对官方文档的总结,只选择了常用的命令):
* SELECT...FOR UPDATE 或 SELECT...LOCK IN SHARE MODE:首先对扫描过的行加锁(实际上是对索引的记录上加锁),如果扫描过的行不满足 WHERE 条件则释放锁(但是有时候,锁的释放不及时比如 UNION 操作下被扫描过的行可能会被放到临时表里,那就直到查询结束才会释放锁);
* ALTER TABLE ... LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE}:在指定的表上施加读锁或者排他锁;
* CREATE TABLE...SELECT...:其中的 SELECT 操作符合 SELECT 语句的枷锁规则,只是不能带有 FOR UPDATE 子句;
* DELETE FROM... WHERE...:在索引项上加排他的 Next-key 锁;
* INSERT:在被插入的索引上加记录锁(意向锁);
* INSERT... ON DUPLICATE KEY UPDATE:在被插入的索引项上加排他 Next-key 锁;
* INSERT INTO T SELECT ... FROM S WHERE ...:对于被插入到 T 中的元组,在对应的索引项上施加排他记录锁。如果隔离级别是 RC ,则在表 S 对应的索引项上不加锁,这是一个一致性读操作;否则加上共享 Next-Key 锁;
* SELECT ... LOCK IN SHARE MODE:在索引上施加共享 Next-Key 锁;
* SELECT ... FROM ... FOR UPDATE:在索引项上施加排他 Next-Key 锁,这样的锁会阻塞上一种”SELECT ... LOCK IN SHARE MODE“操作,但不会阻塞下一种 ”SELECT ... FROM“ 这样的一致性读操作;
* SELECT ... FROM 通常作为一个一致性读操作,不需要加任何锁。但是如果隔离级别是 S,那么也要在索引项上加对应的共享 Next-Key 锁;
* UPDATE ... WHERE ...:在索引项上施加排他的 Next-Key 锁。
* 如果一个表上定义了外键约束,那么在出发约束条件被检查的元组对应的索引项上,任何操作都会施加共享Next-Key 锁。
LOCK TABLES 命令是在表级锁,这个实现是 MySQL Server层的实现,InnoDB 则不会操作,那么如果 InnoDB 不知道 MySQL Server设置了表级锁,就还可能出现一个死锁问题,实践中需要注意。
三、InnoDB 的MVCC原理
在基于锁的并发控制的基础之上,实现了 MVCC 技术。
首先还是强调一点,MVCC 技术本身思想从名字就可以看的出来,就是通过多个版本进行并发的控制。那么并发控制技术,是需要配合其他的并发控制技术来具体实现,这里我们讲的 InnoDB 的 MVCC 原理,就是基于锁的。
3.1 日志
日志是保证事务的原子性、持久性的重要技术之一
,在 MVCC
的实现中也是要用到的,这里简单介绍一下,对于每一个 SQL 操作,都不是一下子执行完成,因此数据的状态都要变化,那么把这个过程记录下来,出现问题进行”回放“,就能应对事物的原子性和持久性。
需要记录的数据通常包括:
事务标识:比如事务 id;
数据项的标识;
旧值:数据项被修改之前的值,又称为 前像;
新值:数据项被修改之后的值,又称为后像。
一系列的 SQL 操作过程变成一个序列,这就是日志,数据库引擎在具体实现的时候会把日志放到日志缓存区,然后刷出到外存,存放到日志文件。
日志文件一般分为 REDO
日志和 UNDO
日志:
REDO 日志
记录事务的标识、数据项的标识和 新值;UNDO 日志
记录事务的标识、数据项的标识和 旧值。(InnoDB 的 MVCC 用到的是 UNDO log)
3.2 InnoDB 的MVCC
因为 InnoDB
的多版本,指的是 行(元组) 级别的版本,在每行(或者每个元组、每条记录)上,都有一些和并发、回滚相关的隐含字段,分别为:
DB_TRX_ID
:很好理解,就是 id,表示上一个执行(insert | update)
操作的事务。至于delete操作,InnoDB 认为是一个 update 操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。DB_ROLL_PTR
:就是pointer
,回滚指针,指向的就是一个旧版本。那么其实指向的是当前记录行的 undo log 信息,是旧版本的数据位于回滚段中的位置,通过这个指针能够找到旧版本;DB_ROW_ID
:随着新行插入而单调递增的行 ID,和 MVCC 关系不大。
在回滚段里的 UNDO 日志分为两种:
INSERT UNDO logs
:插入到回滚段中的日志,仅用于事务提交时使用,当事务提交,则插入 UNDO 日志里的内容被清除;UPDATE UNDO logs
:被用于一致性无锁读,为一致性读提供快照隔离下的可被读取的老版本数据。当没有需要满足一致性读的快照时,一些老版本数据才能被清理。
以上,实现的原理基本告一段落,但是 InnoDB 的实现层面,还有另一个数据结构,就是 Read View
快照。
Read View(读视图)
,跟快照、snapshot 是一个概念,可以看作事务的生命周期里面的一段,而不同的快照就是不同的段。在源码层面,他是一个类,名叫 ReadView,这里面的内容,重点有两个:一个就是保存了快照的左右边界
另一个是提供了如何判断当前行(元组)的可见性的标志。
3.3 和 MVCC 有关的额外两个概念
快照读(snapshot read)
:普通的 select 语句(不包括 select ... lock in share mode, select ... for update)。也就是不加锁的非阻塞读,所以在串行级别下的快照读会退化成当前读。他是基于多版本的,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。当前读(current read)
:select ... lock in share mode,select ... for update,insert,update,delete 语句。(为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。)
对于 InnoDB 的 MVCC
实现,很多博客和书都是架空写的原理,看了《高性能MySQL》,里面也写的是基于每个事务操作的时候给该行添加两个版本号(当前版本号、删除版本号),然后事务操作的时候根据版本号来判定是否执行完毕还是回滚等规则。现在看来并不是这样,但是从上面提到的、源码里实际增加的字段来看,思想是大概类似的,不过实现的机制更加复杂。
四、MVCC 原理总结
4.1 总结 MVCC 原理
到这一步,概念有点多,我们来梳理一下。
首先,事务的概念有了,事务的特性随着概念出来:
ACID
。那么,并发事务如果不加控制,就会存在问题,按处理的难易程度从低到高:丢失更新-脏读-不可重复读-幻读。
于是,数据库如果实现事务,就要保证特性,解决对应的问题,应运而生四个隔离级别:
RU-RC-RR-S
。因为事务是在存储引擎层实现的,所以接下来讨论了基于 InnoDB 引擎的 MySQL 事务的实现:
额外的几个字段;
基于
Undo log
;结合
Read View
(快照)。相关概念:锁的分类:表锁、行锁。读锁、写锁、意向锁。间隙锁(Gap Locks)、Next-Key锁;
相关概念:锁的实施细则;
相关概念:基于锁的并发版本控制,结合 MVCC。如果没有 MVCC ,四个并发问题,除了读读不用加锁,读写、写读、写写都不能并发执行,否则就会产生问题,效率低下,有了MVCC,读写和写读可以并发执行。那 MVCC 都用到了什么呢?
现在我们回过头看一下MVCC机制工作的两个隔离级别:RC 和 RR :
RC
,提交读,要解决脏读的问题,保证一个事务读取到的一定是另一个事务的已经提交的结果,而不能是未提交的结果。那么,对于 RC级别来说,务中,每次快照读都会新生成一个快照和Read View,保证每个事务可以看到别的事务提交的更新。(关于具体的实现还有相应的算法,可见性之类的);RR
,可重复读,要解决不可重复读的问题,保证快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。
4.2 MVCC 解决幻读问题了吗?
在 RR
级别下,没有完全解决幻读 的问题。
我们回忆幻读的概念:一个事务按照相同的查询条件查两次,第一次查出了A集合,第二次却不是了,因为其他事务插入了数据,正好满足这个事务的查询条件。
那么对于上面的 MVCC 原理,基于快照读的情况:
事务 A 开始后,执行普通 select 语句,创建了快照;
事务 B 执行 insert 语句;
事务 A 再执行普通 select 语句,得到的还是之前 B 没有 insert 过的数据,因为这时候 A 读的数据是符合快照可见性条件的数据。
这是解决了幻读问题的。
但是考虑这种情况:
事务A执行的不是普通 select 语句,而是 select ... for update 等语句,根据上面的定义,事务 A 是当前读,每次语句执行的时候都是获取的最新数据。
那么 B 执行 insert语句;
A 再次查询的时候,就可能会查到多一条数据,产生幻读。
这个时候就要出场我们在前面的锁分类部分的一行红字,另外两个锁:间隙锁和 Next-key Locks。
间隙锁在 RR 级别发挥作用,结合行级锁称为 Next-key locks,解决幻读问题。具体的算法可能很复杂,原理就是锁定范围的设置加上了间隙,这样插入操作肯定是没办法进行的,因此就不会存在其他事务的插入操作导致幻读了。
到这里我们可以得出结论,MySQL 里完全解决幻读的方法有两个:
直接使用 S 隔离等级完全串行化;
RR 的隔离级别结合 MVCC 机制,还要结合 间隙锁。
五、其他
关于数据库的锁,服务器层面的实现默认是有表级别的锁,没有行锁(书上没有提这个点,但是网上也有说法讲服务器层面也实现了行锁);
不同的存储引擎层面又以自己的方式实现了不同粒度级别的锁,因此选择引擎不同的时候,我们使用的锁,了解的原理都是基于引擎的,另一方面,锁总和事务联系在一起讨论,不同的存储引擎是否支持事务又是不一样的,所以应该是把server层关于这一块忽略掉了。
感谢点赞支持下哈