本文不再阐述数据库的ACID,请了解后再来阅读此文!

一、Mysql中的锁

首先我们要知道mysql锁,锁住的是索引,当不设置索引的时候会将隐藏字段设置为索引,且隐藏字段默认走全表扫描,所以当不设置主键索引,且表中无其他索引(不包括隐藏字段)时,会锁全表。
下面,我们具体来了解一下Mysql中具体有哪些锁以及锁的作用:

Shared Locks(共享锁/S锁)

若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

Exclusive Locks(排它锁/X锁)

若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

注意:排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是不加锁的就没办法控制了。

Record Locks(记录锁)

记录锁,顾名思义,是加在具体某条记录上的锁。比如select * from user where id=1 and id=10 for update,就会在id=1和id=10的索引行上加Record Lock。

Gap Locks(间隙锁)

间隙锁,它会锁住两个索引之间的区域。比如select * from user where id>1 and id<10 for update,就会在id为(1,10)的索引区间上加Gap Lock。

Next-Key Locks(临界锁)

也叫间隙锁,它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from user where id>=1 and id<=10 for update,就会在id为[1,10]的索引闭区间上加Next-Key Lock。

此外,还有Innodb自带的两种锁,只能由系统自己添加
意向共享锁和意向排他锁
作用:查看该表是否之前已经加了共享或者排他锁提高加表锁的效率,这样在加表锁之前就不用全表扫描是否有数据已经加了共享锁或者是排他锁。

二、MVCC

在了解MVCC之前,首先来先了解一下Mysql中的日志:undo log和redo log
undo log会记录在未执行操作前mysql数据的状态
redo log会记录新数据的备份,将数据恢复到最新状态

回到重点,什么是MVCC?

1.MVCC (Multiversion Concurrency Control) 中文全称叫 多版本并发控制 ,是现代数据库(包括 MySQL 、 Oracle 、 PostgreSQL 等)引擎实现中常用的处理读写冲突的手段, 目的在于提高 数据库 高并发场景下的吞吐性能 。如此一来不同的事务在并发过程中, SELECT 操作可以不加锁而是通过 MVCC 机制读取指定的版本历史记录,并通过一些手段保证保证读取的记录值符合事务所处的隔离级别,从而解决并发场景下的读写冲突。

2.InnoDB 中 MVCC 的实现方式为:每一行记录都有两个隐藏列: trx_id 、roll_ptr (如果没有主键,则还会多一个隐藏的主键列row_id)。

Trx_id
记录最近更新这条行记录的事务 ID ,大小为6个字节

Roll_ptr
表示指向该行回滚段 (rollback segment) 的指针,大小为 7 个字节, InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。

Row_id
行标识(隐藏单调自增 ID ),大小为 6 字节,如果表没有主键, InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息( record header )里都有一个专门的 bit ( deleted_flag )来表示当前记录是否已经被删除。

在多个事务并行操作某行数据的情况下,不同事务对该行数据的 UPDATE 会产生多个版本,然后通过回滚指针组织成一条 Undo Log 链,这节我们通过一个简单的例子来看一下 Undo Log 链是如何组织的,Trx_id和 Roll_ptr两个参数在其中又起到什么样的作用?
1)对Row_id= 1 的这行记录加排他锁
2)把该行原本的值拷贝到undo log 中,Trx_id 和Roll_ptr都不动
3)修改该行的值这时产生一个新版本,更新Trx_id为修改记录的事务 ID ,将 Roll_ptr指向刚刚拷贝到undo log 链中的旧版本记录,这样就能通过Roll_ptr找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE , Undo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁
4)记录 redo log ,包括 undo log 中的修改

那么 INSERT 和 DELETE 会怎么做呢?其实相比 UPDATE 这二者很简单, INSERT 会产生一条新纪录,它的Trx_id为当前插入记录的事务 ID ; DELETE 某条记录时可看成是一种特殊的 UPDATE ,其实是软删,真正执行删除操作会在 commit 时,Trx_id则记录下删除该记录的事务 ID 。

而下面所说的已提交读和可重复读的区别就在于它们生成ReadView的策略不同。
3.实现一致性读----ReadView

1)RC下ReadView的生成

在 RC 隔离级别下,每个 SELECT 语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView 。二者的区别就在于生成 ReadView 的时间点不同,一个是事务之后第一个 SELECT 语句开始、一个是事务中每条 SELECT 语句开始。

ReadView 中是当前活跃的事务 ID 列表,称之为 m_ids ,其中最小值为 up_limit_id ,最大值为 low_limit_id ,事务 ID 是事务开启时 InnoDB 分配的,其大小决定了事务开启的先后顺序,因此我们可以通过 ID 的大小关系来决定版本记录的可见性,具体判断流程如下:

1.如果被访问版本的 trx_id 小于 m_ids 中的最小值 up_limit_id ,说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问。

2.如果被访问版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id ,说明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。需要根据 Undo Log 链找到前一个版本,然后根据该版本的 trx_id 重新判断可见性。

3.如果被访问版本的 trx_id 属性值在 m_ids 列表中最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的 DB_TRX_ID 再从头计算一次可见性;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

4.此时经过一系列判断我们已经得到了这条记录相对 ReadView 来说的可见结果。此时,如果这条记录的 delete_flag 为 true ,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
Mysql的锁以及MVCC解决事务隔离级别

2)RR下ReadView的生成

在 RR 隔离级别下,每个事务 touch first read 时(本质上就是执行第一个 SELECT 语句时,后续所有的 SELECT 都是复用这个 ReadView ,其它 update , delete , insert 语句和一致性读 snapshot 的建立没有关系),会将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView 。
下图中事务 A 第一条 SELECT 语句在事务 B 更新数据前,因此生成的 ReadView 在事务 A 过程中不发生变化,即使事务 B 在事务 A 之前提交,但是事务 A 第二条查询语句依旧无法读到事务 B 的修改。
Mysql的锁以及MVCC解决事务隔离级别
下图中,事务 A 的第一条 SELECT 语句在事务 B 的修改提交之后,因此可以读到事务 B 的修改。 但是注意,如果事务 A 的第一条 SELECT 语句查询时,事务 B 还未提交,那么事务 A 也查不到事务 B 的修改。
Mysql的锁以及MVCC解决事务隔离级别
3)举例
举个例子:
RC下的MVCC判断流程
我们现在回看刚刚的查询过程,为什么事务 B 在 RC 隔离级别下,两次查询的 x 值不同。 RC 下 ReadView 是在语句粒度上生成的。

当事务 A 未提交时,事务 B 进行查询,假设事务 B 的事务 ID 为 300 ,此时生成 ReadView 的 m_ids 为 [200,300],而最新版本的 trx_id 为 200 ,处于 m_ids 中,则该版本记录不可被访问,查询版本链得到上一条记录的 trx_id 为 100 ,小于 m_ids 的最小值 200 ,因此可以被访问,此时事务 B 就查询到值 10 而非 20 。
待事务 A 提交之后,事务 B 进行查询,此时生成的 ReadView 的 m_ids 为 [300],而最新的版本记录中 trx_id 为 200 ,小于 m_ids 的最小值 300 ,因此可以被访问到,此时事务 B 就查询到 20

RR下的MVCC判断流程
如果在 RR 隔离级别下,为什么事务 B 前后两次均查询到 10 呢? **RR 下生成 ReadView 是在事务开始时,m_ids 为 [200,300],后面不发生变化,**因此即使事务 A 提交了, trx_id 为 200 的记录依旧处于 m_ids 中,不能被访问,只能访问版本链中的记录 10

该段参考自:https://www.codercto.com/a/88775.html

三、事务隔离级别的实现(综合运用锁和MVCC)

事务隔离机制的实现主要分以下两种实现方式:

1.在读取数据前,对其加锁,阻止其他事务对数据进行修改,即LBCC
2.生成一个数据请求时间点的一致性数据快照,并用快照来提供一定级别的一致性读取,即MVCC

事务隔离机制的实现

1.READ COMMITTED:InnoDB在该隔离级别(READ COMMITTED)写数据时,使用排它锁, 读取数据之前必须先对其加共享锁,读完后即可释放共享锁。保证读取时数据不被修改,从而避免“脏读”。或者不加锁而是使用了MVCC机制。
2.REPEATABLE READ:同样,InnoDB在该隔离级别(REPEATABLE READ)写数据时,使用排它锁, 读取数据读取数据之前必须先对其加共享锁,直到事务结束才释放锁。等到事务结束才释放这样就能保证可重复读。或者不加锁而是使用了MVCC机制。
而这两种机制的MVCC具体实现参考第二点
因为不管是共享锁还是排他锁都只针对修改数据时的情况,但是并不针对增加,删除的情况,故两种情况都会产生“幻读”。
(实际上,为了实现高并发性,在读的时候一般不加锁,而是使用MVCC机制)
3.SERIALISABLE:该级别下,会自动将所有普通select转化为select … lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。

===============================================================================
此外,还有以下补充点:
事务的原子性是通过undo log来实现
事务的持久性是通过redo log来实现
事务的隔离性是通过读写锁+MVCC来实现
事务的一致性是通过原子性,隔离性,持久性来实现

讨论:曾在一些视频中看到:“实际操作中InnodB在可重复读就已经解决幻读”,原因是Innodb在可重复读使用了临界锁解决了幻读问题,大家觉得对不对呢?

欢迎在评论区留下原因+看法

分类:

技术点:

相关文章: