MySQL锁机制解析 + 行级锁加锁过程中锁退化剖析

MySQL锁分类概念

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

全局锁

全局锁下,整个数据库就处于只读状态

全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

        但是实际上,上全局锁来实现全库逻辑备份业务的方案不太好,因为如果数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。

        解决方案:如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。

备份数据库的工具是 mysqldump,在使用 mysqldump 时加上 –single-transaction 参数的时候,就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。

        InnoDB 存储引擎默认的事务隔离级别正是可重复读,因此可以采用这种方式来备份数据库。

表级锁

MySQL 里面表级别的锁有这几种:

  • 表锁:划分为共享读锁和独占写锁两种

    • 共享读锁:所有线程(包括上锁的线程)都只能读,都不能写

    • 独占写锁:上锁的线程可以读写,其他线程啥都不能做

  • 元数据锁:元数据锁是在执行 DDL操作时由数据库自动加入的,并且会一直保持锁状态,直到事务完成才会自动释放。

    • 对一张表的数据进行 CRUD 操作时,加的是 MDL 读锁;在这期间,如果有其他线程要更改该表的结构,那么将会被阻塞

    • 对一张表做结构变更操作的时候,加的是 MDL 写锁;在这期间,如果有其他线程执行了 CRUD 操作,那么将会被阻塞

      那如果数据库有一个长事务(所谓的长事务,就是开启了事务,但是一直还没提交),那在对表结构做变更操作的时候,可能会发生意想不到的事情,比如下面这个顺序的场景:

      1. 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 select 语句,此时就先对该表加上 MDL 读锁;

      2. 然后,线程 B 也执行了同样的 select 语句,此时并不会阻塞,因为「读读」并不冲突;

      3. 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞,

             那么在线程 C 阻塞后,后续有对该表的 select 语句,就都会被阻塞,如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。

             这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。

      所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

  • 意向锁: 意向锁的目的是为了快速判断表里是否有记录被加锁

    •        不加意向锁,加表锁前会对整张表每一行都进行遍历来确定行锁的存在,而意向锁的存在就类似于所有行锁的大家长,加表锁前问一下这个大家长的意见,家长同意(锁兼容)就可以直接加,不同意(锁不兼容)就被阻塞,直到行锁释放

  • AUTO-INC 锁(了解):主键通常都会设置成自增可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放

    • 在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。

    •        但是, AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。

             一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁

行级锁

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

        意思就是说,如果执行 DML 操作你修改的字段是加了索引的,那么你修改该字段所在的行就会被上锁。如果该字段不是添加了索引的,或者索引失效了的,那么你对该行进行修改操作的时候。他锁住的就不是这一行,而是整张表,就会导致效率下降。

  • 行锁:行锁分为共享锁和排他锁两种,执行事务型SQL都会自动上排他锁,执行一般的SELECT不上锁(MVCC的无锁机制),执行 SELECT...LOCKIN SHARE MODE 上共享锁

  • 间隙锁:只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。如果查询涉及到范围条件,MySQL会在这个范围内的索引记录之间的间隙上加上间隙锁,即使查询的记录不存在,只要查询条件指定了范围,MySQL仍然会在这个范围内的间隙上加上间隙锁。间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其他事务在这个区域内插入、修改、删除数据

  • 临键锁:行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap


        很多人都对 MySQL 加行级锁的规则搞的迷迷糊糊,对记录一会加的是 next-key 锁,一会加是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,可以看看下文板块进一步理解,希望能够接触你的疑惑

行级锁加锁步骤剖析

加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间

在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁

唯一索引等值查询

当我们用唯一索引进行等值查询的时候,查询的记录存不存在,加锁的规则也会不同:

对二级索引进行锁定读查询的时候,因为存在两个索引(二级索引和主键索引),所以两个索引都会加锁。

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」

  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后(哪怕是字符型数据也可以比较大小,比较规则和String字符串比较规则一致),将该记录的索引中的 next-key lock 会退化成「间隙锁」

    select * from user where id = 2 for update;
    • 接下来,如果有其他事务插入 id 值为 2、3、4 这一些记录的话,这些插入语句都会发生阻塞。

  • 为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock?

    • 如果是 next-key lock,就意味着其他事务无法删除 id = 5 这条记录,但是这次的案例是查询 id = 2 的记录,只要保证前后两次查询 id = 2 的结果集相同,就能避免幻读的问题了,所以即使 id =5 被删除,也不会有什么影响,那就没必须加 next-key lock,因此只需要在 id = 5 加间隙锁,避免其他事务插入 id = 2 的新记录就行了。

  • 为什么不可以针对不存在的记录加记录锁?

    • 锁是加在索引上的,而这个场景下查询的记录是不存在的,自然就没办法锁住这条不存在的记录。

唯一索引范围查询

范围查询和等值查询的加锁规则是不同的。

当唯一索引进行范围查询时,会对每一个扫描到的索引加 next-key 锁,然后如果遇到下面这些情况,会退化成记录锁或者间隙锁

  • 大于等于范围查询

    • 如果查询条件中包含等值条件,并且表中存在满足这个等值条件的记录,那么这个记录上的next-key锁会退化成记录锁。表中不存在满足这个等值条件的记录,那么就会在维持一个查询条件到终点的临键锁

  • 小于或小于等于范围查询

    • 如果查询条件中包含一个特定的值(例如 id < 10id <= 10 ),我们需要看这个特定值的记录是否存在于表中:

      • 如果这个特定值的记录不存在,那么查询会扫描到范围的最右端记录(即最大的记录),这个最右端记录的next-key锁会退化成间隙锁。

      • 如果这个特定值的记录存在,那么情况会有所不同:

        • 对于id < 10这样的条件,扫描到满足条件的最后一个记录(即id 9)后,由于id 10不满足条件,所以id 9的next-key锁会退化成间隙锁

        • 对于id <= 10这样的条件,扫描到满足条件的最后一个记录(即id 10)时,由于id 10也满足条件,所以不会在这里退化成间隙锁,而是保持next-key锁

非唯一索引等值查询

        当我们用非唯一索引进行等值查询的时候,因为存在两个索引,一个是主键索引,一个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁

  • 查询记录存在的情况

    • 查询过程中,InnoDB会扫描二级索引,直到找到第一个不满足查询条件的记录为止。

    • 在扫描过程中,对于每个满足查询条件的记录,InnoDB会在二级索引上加上next-key锁。

    • 当扫描到第一个不满足条件的记录时,InnoDB会在这个记录的二级索引上将next-key锁退化为间隙锁。

    • 同时,对于每个满足查询条件的记录,InnoDB还会在对应的主键索引上加上记录锁(行锁),以确保这些记录不会被其他事务修改。

  • 查询记录不存在的情况

    • 查询过程中,InnoDB同样会扫描二级索引,直到找到第一个不满足查询条件的记录为止。

    • 由于没有记录满足查询条件,InnoDB不会在主键索引上加锁,因为没有记录需要锁定。

    • 对于第一个不满足条件的二级索引记录,InnoDB会在其上将next-key锁退化为间隙锁。

        总结一下,无论查询的记录是否存在,InnoDB都会在二级索引上加锁,直到找到第一个不满足条件的记录。如果查询的记录存在,InnoDB还会在主键索引上加行锁。间隙锁的作用是防止其他事务在这个范围内插入新的记录,。而行锁则是为了防止满足查询条件的记录被其他事务修改。

非唯一索引范围查询

非唯一索引和主键索引的范围查询的加锁不同,在于非唯一索引范围查询,索引的 next-key lock 不会有退化为间隙锁和记录锁的情况,也就是非唯一索引进行范围查询时,对二级索引记录加锁都是加 next-key 锁。

mysql> select * from user where age >= 22  for update;
+----+-----------+-----+
| id | name      | age |
+----+-----------+-----+
| 10 | 山治      |  22 |
| 20 | 香克斯    |  39 |
+----+-----------+-----+

没有加索引的查询

        在MySQL中,当执行具有加锁性质的查询(如SELECT ... FOR UPDATEUPDATEDELETE等)时,如果查询条件能够利用索引来定位记录,那么MySQL会通过索引来加锁。这种情况下,只会对满足查询条件的记录上的索引加锁,从而避免了对整个表的锁定。

        然而,如果查询条件没有使用索引,或者查询语句由于某些原因没有走索引查询,MySQL将不得不执行全表扫描。在全表扫描的过程中,MySQL会对每一行记录的索引进行扫描,并在这些索引上加上next-key锁。这意味着,即使只查询部分记录,也会对整个表的所有记录的索引加上锁。

        因此,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学徒630

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值