MySQL中的事务原理和锁机制

本文主要总结 MySQL 事务几种隔离级别的实现和其中锁的使用情况。

在开始前先简单回顾事务几种隔离级别以及带来的问题。

四种隔离级别:读未提交、读已提交、可重复读、可串行化。

带来的问题:脏读、不可重复读、幻读。分别是由读未提交、读已提交、可重复读引起的。

脏读:一个事务读取到在另一个事务还未提交时的修改。

不可重复读:一个事务在另一个事务提交前后读取到了不同数据。(侧重于某一条数据,这条数据内容发生了变化)。

幻读:一个事务在另一个事务提交前后读取到了不同数据。(侧重于多了或是少了一条数据)。

在 Mysql 中,默认隔离级别是可重复读,在默认时却一定程度上解决了幻读,为什么这么说呢?请看下面这个例子。

MySQL中的事务原理和锁机制

同时我们查看数据库中的数据:MySQL中的事务原理和锁机制

 可以看到并没有发生 “幻读”,这是为什么?难道可重复读级别已经解决了“幻读”?后面会详细解释。

 

Mysql 中的锁

对于存储引擎 MyISAM ,只支持表级锁,对于 InnoDB 来说,既支持表级锁、也支持行级锁。所以 InnoDB 可以用于高并发的场景下而 MyISAM 不行。

按颗粒度划分

1、行级锁

只对一行数据加锁,当一个事务操作某一行事务时,只对该行数据加排他锁时,其他事务对其他行数据操作时不会影响,并发性好。缺点是在加多条数据时加锁会比较耗时。

使用场景:可串行化隔离级别

2、表级锁

对整张表进行加锁。加锁快但是可承受的并发量低。

3、页级锁

对一页数据进行加锁,介于行级锁与表级锁之间。

 

按种类划分

1、共享锁(读锁)

共享锁是对于MySQL中的读操作的,所以共享锁也叫读锁,一个事务进行读操作时,会对读取的数据添加读锁(可串行化下的读操作是自动加锁的,其他隔离级别需要在查询语句后面添加 lock in share mode),加锁后其他事务也可以对加锁的数据进行读取。

 

2、排他锁(写锁)

排它锁是对于 MySQL 中的写操作的,所以排它锁也叫写锁。添加排它锁的数据其他事务就不能进行操作,同时共享锁与排它锁也是互斥的,也就是一个事务对某数据添加了共享锁,那么其他事务就不能对其再添加排它锁。在所有隔离级别级别中的修改操作(insert、update、delete)都会添加排他锁,而读操作可以通过在语句后面添加 for update 来对读取的数据添加排它锁

 

其他种类

1、Record Lock

记录锁。record lock 是加在具体记录对应聚簇索引上的锁,它是锁住的是索引本身而不是记录,如果该表没有聚簇索引,也会创建一个聚簇索引来代替。换句话说 record lock 属于行级锁。它既可以是共享锁也可以是排它锁(究竟是共享锁还是排他锁上面已经分析了)。任何级别都会存在。

2、Gap Lock

间隙锁,就是加在两条数据索引之间的锁,比如数据表student(id,name),id 是主键,有数据(5,"aa"),(7,"bb"),隔离级别是可串行化。此时事务1执行select * from student where id>5 and id<7,那么就会对 (4,7) 添加间隙锁,锁住中间的间隙。比如说事务2执行insert into(6,"cc"),那么次操作就会被阻塞。在可重复读及以上级别才会有。

3、Next-Key Lock

指的是 Record Lock 与 Gap Lock 的结合。针对 Gap Lock 中的例子,如果事务1执行的是 select * from dept where id>4 and id<8,那么对数据(5,"aa")、(7,"bb")对应的聚簇索引上也会添加 Record Lock。同时(4,5),(5,7),(7,8)也会加上间隙锁。同 Gap Lock 一样,只有可重复读以以上级别才会出现。

 

 

四种隔离级别的实现

在说明原理前,先了解一下什么是快照读和当前读。

快照读Mysql 默认的隔离级别是“可重复读”。通过文章开头的例子可以看出左边事务在右边事务执行修改提交前后查询的数据都一样,左边事务的查询就是一个快照读。快照读的数据可以看作一个快照,其他事务的修改不会改变这个快照值。也就是说快照读的数据不一定是最新值,可重复读级别也因此才保证了 “可重复读”。快照读的优势是不用加锁,并发效率高。

使用场景:在 Mysql 的隔离级别中,除了可串行化级别的读外,其他隔离级别中事务的读都是快照读。

 

当前读当前读指的就是读的是最新值。既然是要求是最新值,那么就需要进行加锁限制,所以当前读是需要加锁的,同时因为当前读一定是最新的数据,所以就无法保证 “可重复读”。

使用场景:首先是可串行化中事务的读操作是当前读,而四种隔离级别中的所有修改(insert、update、delete)操作都属于当前读。可能你觉得读操作和修改操作没有关系,但是事实是这些修改操作是先 “读” 找到数据具体的位置才能进行 “修改”。

 

读已提交和可重复读的实现

这两种隔离级别的实现归功于 MVCC 机制。

MVCC机制

MVCC机制也叫多版本并发控制,用于控制数据库的并发访问。在 Mysql 的 InnoDB 存储引擎中主要作用于实现读已提交和可重复读隔离级别。实现原理是通过 undo日志版本链和 Read View 。

1、undo日志版本链。在 InnoDB 聚簇索引记录的行数据中有两个隐藏列,trx_id 和 roll_pointer,trx_id 表示当前行数据上次被修改的事务 id (事务 ID 是自增的,越新的事务 ID 越大),roll_pointer 是每次在修改完数据前,都会将修改前的数据存入undo log(专门用于记录事务修改前数据的日志系统,用于进行事务的回滚和生成数据快照),roll_pointer 就是当前行数据修改前在 undo 日志中的存储位置。

2、Read View。内部主要有四个部分组成,第一个是创建 Read View 的事务 id creator_trx_id,第二个是创建 Read View 时还未提交的事务 id 集合trx_ids,第三个是未提交事务 id 集合中的最大值up_limit_id,第四个是未提交事务 id 集合中的最小值low_limit_id。

当执行查询操作时会先找磁盘上的数据,然后根据 Read View 里的各个值进行判断,

1)如果该数据的 trx_id 等于 creator_trx_id,那么就说明这条数据是创建 Read View的事务修改的,那么就直接返回;

2)如果大于 up_limit_id,说明是新事务修改的,那么会根据 roll_pointer 找到上一个版本的数据重新比较;

3)如果小于 low_limit_id,那么说明是之前的事务修改的数据,那么就直接返回;

4)如果是在 low_limit_id 与 up_limit_id 中间,那么需要去 trx_ids 中逐个查找,如果存在,就根据 roll_pointer找打上一个版本的数据,然后再判断;如果不存在就说明该数据是创建 Read View 时就已经修改好的了,可以返回。

 

而读已提交和可重复读之所以不同就是它们 Read View 生成机制不同,读已提交是每次 select 都会重新生成一次,而可重复读是一次事务只会在第一次查询时生成一个 Read View。

举个借鉴于网上的例子,比如事务1先修改 name 为小明1,假设此时事务id 是60,那么就会在修改前将之前的50写入 undo log,同时在修改时将生成的undo log 行数据地址写入 roll_pointer,然后暂不提交事务1。开一个事务2,事务 id 为 55,进行查询操作,此时生成的 Read View 的trx_ids是[60],creator_trx_id 为 55,对应的数据状态就是下图,首先先得到磁盘数据的 trx_id ,为60,然后判断,不等于 creator_trx_id,然后检查,最大值和最小值都是 60,所以通过 roll_pointer 从 undo log 中找到 “小明” 那条数据,再次判断,发现 50 是小于 60的,所以满足,返回数据。

MySQL中的事务原理和锁机制

然后提交事务1,再开一个事务3,将name改成小明2,假设此时的事务 id 是100,那么在修改前又会将 trx_id 为 60 拷贝进 undo log,同时修改时将 trx_id 改为100,然后事务3暂不提交,此时事务1再进行select。如果隔离级别是读已提交,那么就会重新生成 Read View,trx_ids是[100],creator_trx_id 为55,判断过程和上面相似,最终返回的是小明1那条数据;而如果是可重复读,那么还是一开始的 Read View,trx_ids 还是[60],creator_trx_id 还是 55,那么还是从小明2 的 trx_id 进行判断,发现不等于 55,且大于60,跳到 小明1 ,对 trx_id判断,还是大于,最终还是返回 “小明” 那条数据。下面是这个例子最终的示意图

MySQL中的事务原理和锁机制 

 

读未提交和可串行化实现

这两个实现比较简单。读未提交就是每次事务执行的修改都更新到对应的数据上,然后读取直接读取这个数据就可以了。而可串行化则是使用了读锁和写锁以及间隙锁来实现的,对会造成“幻读”、“脏读”、“不可重复读” 的操作会进行阻塞,也正因为这样,极易任意造成阻塞,所以不建议使用可串行化级别。

 

 

不同隔离级别下加锁情况

对于不同的隔离级别,不同的列情况,加锁情况都各不不同,下面会列举各个场景下加锁的情况。

1、读未提交级别

读操作不会加锁,写操作会添加排它锁。因为会发生脏读,所以 MVCC并不会发生效果。可以手动添加 for update 、lock in share mode 来加锁,不会产生间隙锁,只有记录锁。

无论是否使用索引,是否是手动添加锁,只会对最终操作的数据加 Record Lock。

 

2、读已提交级别

读操作不会加锁,写操作会添加排它锁。MVCC 会在每次查询时生成 Read View,可以手动添加 for update 、lock in share mode 来加锁,不会产生间隙锁,只有记录锁。

无论是否使用索引,是否是手动添加锁,只会对最终操作的数据加 Record Lock。(在未使用到索引时数据库会对所有数据加锁,当加载到 Server 层筛选后会将不符合条件的数据进行解锁,所以我们会认为只对最终操作的数据加锁,读未提交级别的未使用索引情况也相同)

 

3、可重复读级别

可重复读是一个特殊的隔离级别,为什么这么说呢?因为它是 mysql 默认的隔离级别,因为 "可串行化" 级别默认对读操作加锁,导致程序的并发性不高,所以不建议使用,而可重复读因为使用的是快照读,所以并发性很好,并且解决了不可重复读、脏读以及 "快照读" 幻读,但同时会有 "当前读"幻读的问题产生(下面"MySQL 对幻读的解决" 会详细解释),所以针对这个问题引入了间隙锁来解决。

读操作不会加锁,写操作会添加排它锁。MVCC 会在事务开始第一次查询时生成 Read View,可以手动添加 for update、lock in share mode 来加锁,可能会产生间隙锁。

无论是否是手动添加锁,1)在使用到唯一索引和主键索引时,会对对应记录对应的聚簇索引上添加 Record Lock。

2)在使用非唯一索引时,会对对应数据左右的间隙额外添加间隙锁,也就是使用 Next-key Lock。以下图为例

MySQL中的事务原理和锁机制

 name 为主键,id 为普通索引。当执行 delete from t1 where id = 10 时,由于新增的数据可能在 [ (6,c),(10,b) ] 之间,[ (10,b),(10,d) ] 之间,[ (10,d),(11,f) ] 之间,所以需要对这三个间隙加锁,来防止在事务1操作时其他事务对这三个位置进行其他修改操作导致操作出错。比如现在 insert (10,a),那么就会判定是 [ (6,c),(10,b) ] 之间的,此操作就会被阻塞。

3)如果没有用到索引,那么会对所有数据以及他们两边的间隙进行加 Next-key Lock锁。相当于整张表进行加锁。这也对应着 “索引失效时行级锁会退化成表级锁” 的规律。

MySQL中的事务原理和锁机制

 

4、可串行化级别

读操作会加读锁,写操作会加写锁,读写锁互斥。也会有间隙锁。

1)用到主键索引和唯一索引,会对操作数据添加 Record Lock。

2)普通索引,会对操作数据以及间隙添加 Next-key Lock。

3)未使用索引,会对所有数据以及两边间隙添加 Next-key Lock。

 

 

MySQL 对幻读的解决

“快照读” 幻读

通过上面对 MVCC 原理的解释,可以知道文章开头的例子为什么“解决了” 幻读。如果假设左边的事务1 id 是50,右边事务2 id 是55,其他数据创建时的事务是10,那么在事务1第一次查询时生成的 Read View 的 trx_ids 为[55],对应的数据如下

MySQL中的事务原理和锁机制

 那么在判断其他数据时 trx_id 的10小于 trx_ids 的最小值55,所以通过,而 id 为6的数据发现 trx_id 正好等于 55,所以获取 roll_point 从 undo log中找到之前的数据快照,但是发现该列值为空,所以放弃跳到下一条数据。没有出现文章开头所说的 “幻读” 情况,开头所说的读就叫做 “快照读” 幻读。 由此我们可以知道, MVCC 可以解决 “快照度” 幻读。

这里可以再说一下题外话,其实对于 MVCC 中可重复读级别 Read View 创建时机为什么是第一次查询时生成而不是事务启动时就生成,可以通过下面的测试来证明。

MySQL中的事务原理和锁机制

 可以在事务2提交后再查询就会查出提交后的数据。

 

“当前读” 幻读

这样看来 MVCC 已经解决了幻读问题,而在一开始也说过在默认时在一定程度上解决了幻读,为什么这么说?请看下面这个例子

MySQL中的事务原理和锁机制

 如果单看左边的事务,会发现明明表中没有id为6的记录,但是就是无法执行 insert 操作,显示主键已存在。这就是 “当前读”幻读,而 MVCC只能解决 “快照读” 幻读。由于前面对 “当前读”、“快照读” 的解释可以知道这两种读是互斥的,那么如何解决 “当前读” 幻读。第一种方式是直接切换成 “可串行化” 级别,这种因为默认对数据加锁,不利于项目的并发执行,所以不建议;第二种就是手动添加锁,在特定的操作后添加 for update 或 lock in share mode。这样就可以实现 "当前读"了。

 

 

死锁

MySQL 的死锁与多线程中的死锁本质上一样,其核心思想就是 “两个及以上的事务互相获取对方事务添加的锁记录(排它锁)”,

MySQL中的事务原理和锁机制

 MySQL中的事务原理和锁机制

 

 

博客主要灵感来源于

https://blog.csdn.net/cug_jiang126com/article/details/50596729

https://www.cnblogs.com/crazylqy/p/7611069.html,其中一些图片和加锁情况来源于第二个

MySQL中的事务原理和锁机制

上一篇:Zabbix 使用ODBC监控数据库错误处理


下一篇:MySql 条件查询