MySQL中的事务原理和锁机制

您所在的位置:网站首页 mysql存储引擎原理 MySQL中的事务原理和锁机制

MySQL中的事务原理和锁机制

#MySQL中的事务原理和锁机制| 来源: 网络整理| 查看: 265

本文主要总结 MySQL 事务几种隔离级别的实现和其中锁的使用情况。因为 Mysql 几种存储引擎中 InnoDB 使用的最多,同时也支持事务和锁,所以这篇主要说得是 InnoDB 引擎下的锁机制与事务。

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

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

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

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

不可重复读:一个事务在另一个事务提交前后读取到了不同数据。

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

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

同时我们查看数据库中的数据:

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

 

Mysql 中的锁

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

按颗粒度划分

1、行级锁

只对一行数据加锁,当一个事务操作某一行事务时,只对该行数据加排他锁时,其他事务对其他行数据操作时不会影响,并发性好。缺点是在加多条数据时加锁会比较耗时。一个事务获取到锁后直到事务提交才会释放锁。

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

2、表级锁

包含两种。1、MDL,是 DDL 操作与 DML、DQL 操作冲突的加锁。下面会讲解。

2、对整张表进行加读写锁,仅针对 DML、DQL 操作。加锁快但是可承受的并发量低。       加读锁:lock table 表名  read ;          加写锁:lock  tables  表名  write ;         释放读写锁:unlock  tables;

3、全局锁

对所有表所有数据进行加锁。这个锁是读锁,也就是加锁后当前数据库只能处理读操作,不能处理写操作。一般在将整个库的数据进行逻辑备份时使用(在 InnoDB 中可以使用 mysqldump 进行非阻塞式备份,原理就是通过 隔离级别MVCC数据一致性实现的)。    加锁: Flush tables with read lock (FTWRL)

这里不建议使用 set global readonly = true 来代替 FTWRL,因为  1、readonly 参数在某些系统业务中是用来判断当前库是主库还是从库,如果修改会对业务操作影响  2、在发生异常时 readonly 不会改变而 FTWRL 会自动释放锁,未改变的话在恢复后就无法处理写操作,导致系统出错。

4、页级锁(存储引擎BDB,不常使用)

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

 

 

按种类划分

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 id4 and id5 不行,必须包含等于的符号),那么根据条件找到第一个满足的记录值5正好等于条件的边界值 5,那么也可以优化成行锁),给唯一索引加锁的时候,next-key lock 会退化成行锁。

  优化2:索引上的等值查询(符号必须是=),向右遍历时且最后一个值不满足等值条件的时候,next-key lock 会退化成间隙锁。

  一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。

  除了上面五种情况外,还有一些场景需要注意:1、因为默认的排序方式是升序,如果声明为 desc 那么就需要从高向低匹配。  2、上面说的都是等值,如果是不等值,如 >、=、9 and id10 ,找到满足的第一个值,也就是 id=15,这时因为原则1,添加 next-key lock(10,15 ],然后向右遍历,找到 id=15 后就已经满足了后置条件,所以最终的锁就是(10,15 ]。但是实际并不是如此。sessionB、sessionC都被锁住了。

实际的加锁:在添加完 next-key lock(10,15 ] 后,还会匹配直到不满足条件为止,也就是将非唯一索引的加锁规则弄到了唯一索引上。所以在遍历到 id=20 后才停止,所以相对应的又添加了一个 mext-key lock(15,20 ]。

6、使用 limit x 会在加锁的元素达到 x 后就会停止加锁。加入增加了一行记录 (30,10,10),执行 SQL :delete  from  t  where  c=10  limit  2;   c是索引列,那么其加锁情况如下:

加锁过程:首先匹配到(10,10,10)这一行记录,然后添加 next-key lock((5,5),(10,10) ],这是第一个值,然后向右匹配,找到 (30,10,10),添加 next-key lock((10,10),(10,30) ]。到这两个值都已经匹配完成,所以即使是非唯一索引,也不会再向右边匹配。

7、使用 in 就相当于三条等值查询。执行SQL:select id from t where c in(5,20,10) lock in share mode;

加锁情况:因为是默认升序的,所以执行是先执行 c=5,然后 c=10,最后 c=20。c=5加锁是(0,5 ]、(5,10)(因为优化2);c=10加锁是(5,10 ]、(10,15);c=20 加锁是(15,20 ]、(20,25)。

8、特殊情况1。删除操作可能会使间隙锁范围扩散。

sessionA 原本添加的锁是一个 next-key lock(10,15 ]。所以 sessionB 删除是不会被阻塞的,但是在删除后因为间隙失去了,就会转移到前一个点上,也就是间隙扩大,所以锁就变成了(5,15 ]。

9、特殊情况2。更新操作也可能会使间隙锁范围扩散。

更新操作可以看成两步:先新增修改后的记录,然后再删除要修改的记录。

分析:sessionA 加锁区间是(5,+∞)所以这两步过程如下:

第一步:先增加c=1的记录,删除c=5的记录,锁区间因为删除扩大变成(1,+∞);

第二步:增加 c=5的记录,被锁阻塞。

10、空表的加锁情况是 next-key lock (-∞, supremum]。

 

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],对应的数据如下

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

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

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

 

“当前读” 幻读

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

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

 

 

死锁

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

 

 解决死锁的方式有两种:

1、设置事务超时时间。通过修改 innodb_lock_wait_timeout 参数来修改事务的超时时间,默认为 50s。这种方式不推荐使用,因为并不能根本上解决问题,缩短时间虽然能解决死锁,但是对于执行时间长的事务来说就永远无法完成。

2、开启死锁检测,在事务获取锁资源时阻塞就会开始检测,如果发生死锁就会回滚其中一个事务。这是默认的方式,死锁检测可以通过将 innodb_deadlock_detect 设为 on 来开启(默认开启)。但是死锁检测会消耗大量的 CPU。所以可以进行一些优化。

优化方式:

1、在保证当前系统不会发生死锁后可以直接关闭搜索检测。

2、控制并发度,通过使用中间件来减少数据库操作的并发量来提高效率。

3、将操作的数据拆分成多个数据,然后依次获取,这样就减小了资源冲突的概率,但是需要结合业务判断是否允许拆分数据。

 

Next-key Lock 是 Record Lock 与 Gap Lock 结合的验证 CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);

可重复读隔离级别下

Gap Lock 是共享锁,但是行锁、Next-key Lock不是,如果这样的话因为 sessionA 开始加的锁就是 Next-key Lock( (5,5,5) , (10,10,10) ],间隙锁 ( (10,10,10) , (15,15,15) )。所以 sessionB 应该是无法获取 Next-key Lock( (5,5,5) , (10,10,10) ],所以 sessionA 的 insert  语句就不应该被阻塞,也就不会造成死锁,但为什么会这样呢?这是因为 Next-key Lock 并不是一把原子性的锁,它是由 Gap Lock 与 Record Lock 组成的,在 sessionB 的 update 语句执行时首先添加的是间隙锁( (5,5,5) , (10,10,10) ),然后再尝试添加(10,10,10) 的行锁,然后失败,随后后面的间隙锁也会被阻塞加锁。所以最终加锁范围就是 ( (5,5,5) , (10,10,10) )。所以 sessionA 在执行 insert 时也会等待 sessionB 释放间隙锁后才能执行,导致死锁。

而如果把 sessionA 的insert 操作改成 update t set d=d+1 where c=10; 会发现还是阻塞发生死锁,这是因为 sessionA 开始加的是共享锁,只能读不能写,如果改成 select * from t where c=10 for update 就不会发生死锁了。

 

博客总结来源于

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

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

以及极客时间的



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3