解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器

在这里插入图片描述

  • 前言
  • 并发事务问题
  • 锁分类
  • 锁定读
    • 共享锁
    • 排它锁
  • 意向排它、意向共享锁
  • 自增锁
  • 记录锁
  • 间隙锁
  • InnoDB 行锁模式及加锁方法
  • 死锁
  • 总结

前言

MySQL 锁机制比较显而易见,其最显著的特点是不同的存储引擎支持不同的锁机制

MySQL InnoDB 锁机制官方文档

比如在 MyISAM、Memory 存储引擎采用的是表级锁(table- level locking)InnoDB 存储引擎既支持行级锁(row-level locking)也支持表级锁,但默认情况下是采用行级锁

  • 表锁:开销小、加锁快,不会发生死锁,锁定的粒度大,发生锁冲突的概率最高,并发度最低
  • 行锁:开销大、加锁慢,会发生思索,锁定的粒度最小,发生锁冲突的概率最小,并发度最高

从锁的角度来看,表锁更适合以查询为主,只有少量按索引条件更新数据的应用,如 PC Web 后台应用;行级锁则更适合有大量按索引条件并发更新少量的数据,同时也有并发查询的应用,如在线事务处理(OLTP)系统、小程序 C 端、App C 端系统等

并发事务问题

并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户的并发操作,但与此同时,会发生脏读、不可重复读、幻读问题

解决这种问题一般有两种可选的方案,如下:

  1. 读操作 MVCC,写操作进行加锁 > 事务利用 MVCC 进行读取称之为一致性读或快照读、无锁读,一致性读并不会对表中任何记录作加锁操作,其他事务可以自由对表中记录作改动

采用 MVCC 方式,读、写操作彼此不冲突,性能更高,采用加锁的方式,读、写操作彼此需要排队执行,从而影响性能;但是在某些情况下,还是需要采用加锁的方式去执行,比如:要通过间隙锁来解决不可重复读隔离级别的幻读问题

  1. 读、写操作进行加锁 > 适用场景:业务场景不允许读取记录的旧版本,每次都必须读取记录的最新版本

比如在银行存款事务中,需要先把账户余额读取出来,然后再将其加上本次存款的金额,最后再写入到数据库中。再将账户余额读取出来的这个过程,不想让其他的事务再访问此余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。
此场景意味着读取数据时也要进行加锁操作,读、写操作并行执行,也要像写写操作那样依此排队执行

关于 MySQL MVCC 机制更多内容,可以阅读此文章:基于 MySQL 事务、隔离级别及 MVCC 机制详细剖析

锁分类

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第1张图片

共享锁:Shared Locks,简称 S 锁,属于行锁

排它锁:Exclusive Locks,简称 X 锁,属于行锁

意向锁:意向共享锁+意向排它锁的组合

意向共享锁:Intension Shared Locks,简称 IS 锁,属于表锁

意向排它锁:Intension Exclusive Locks,简称 IX 锁,属于表锁

自增锁:AUTO-INC Locks,在处理自增长列时的锁定行为

临键锁:记录锁+间隙锁的组合

记录锁:Record Locks,仅仅把一条记录上锁

间隙锁:Gap Locks,对索引前后的间隙上锁,不对索引本身上锁,简称 Gap 锁

锁定读

锁定读(LockingReads)也称为当前读 LBCC(基于锁的并发控制 > Lock-Based Concurrency Control)读取的是最新版本,对读取的记录加锁,阻塞其他事务同时改动相同的记录,避免数据安全问题

共享锁:lock in share mode、排它锁:for update、update、delete

通过以下 SQL 语句,进行共享锁、排它锁的案例演示:

CREATE TABLE student (
	id INT ( 10 ) PRIMARY KEY auto_increment,
	`name` VARCHAR ( 20 )
) ENGINE = INNODB;

INSERT INTO student VALUES 
( 1, 'vnjohn' ),
( 2, 'zhangsan' ),
( 3, 'lisi' ),
( 4, 'wangwu' );

共享锁

多个事务对同一条数据可以共享一把锁,但只能读不能写

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第2张图片

会话 vnjohn-transaction1:

begin;
select * from student where id=1 lock in share mode;

会话 vnjohn-transaction2:

begin;
# 读取数据没有问题
select * from student where id=1; 
# 注意:无法修改会卡死,
# 当会话 1 commit 提交事务之后,会立刻修改成功
update student set name ='vnjohn' where id=1;

当会话 1 执行查询 + 了共享锁,会话 2 对该条记录进行更新操作,会阻塞住,直到会话 1 事务提交,才会立刻修改成功,假如会话 1 出现慢 SQL 数据一直查询不出来,那么就会出现错误: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

总之,共享锁,读、读操作不互斥,读、写操作互斥

排它锁

排它锁不能与其他锁并存,若一个事务获取一个数据行的排它锁,其他事务就不能再次获取该行的其他锁,只有获取了该数据行的排它锁所在事务才能对数据进行读取、写入操作

update、delete 语句默认就是排它锁

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第3张图片

会话 vnjohn-transaction1:

begin;
select * from student where id=1 for update;

会话 vnjohn-transaction2:

begin;
select * from student where id=1 for update;
select * from student where id=1 lock in share mode;

当会话 1 执行查询 + 了排它锁,会话 2 对该条记录加排它锁、共享锁操作,都会被阻塞住,直到会话 1 事务提交,才会加锁成功

意向排它、意向共享锁

意向锁是一种粒度更粗的锁,用于协调并发事务对表和表中行的锁定。它们并不直接锁定行,而是指示事务在某个层次上有意向获取特定类型的锁。意向锁的引入可以减少冲突,提高并发性能

意向排它锁:表示事务有意向在某个表或表分区上获取排它锁(Exclusive Lock)一个事务在获取某个表的排它锁之前,必须先获取该表的意向排它锁

意向共享锁:表示事务有意向在某个表或表分区上获取共享锁(Shared Lock)多个事务可以同时持有同一个表的意向共享锁,但在获取某个表的排它锁之前,必须先释放该表的意向共享锁

意向锁的引入有助于优化锁定算法,避免了不必要的冲突,提高了并发性能。在事务操作过程中,当需要获取某个表的排它锁或共享锁时,先检查是否存在对应的意向锁,以减少对其他事务的干扰

意向锁是隐式获取和释放的,并不需要显式的锁定语句来处理,它们是由 InnoDB 存储引擎自动管理的

自增锁

Auto-Increment:自增长列的特殊锁机制,通过 innodb_autoinc_lock_mode 参数配置自增长列的锁定模式,它决定了在插入数据时,如何对自增长序列进行锁定

innodb_autoinc_lock_mode 该参数有几个可选值,如下:

  1. 0(Traditional): 表示使用传统的自增锁定方式,在插入数据时,会对整个表进行排它锁定,以防止并发插入导致自增值的冲突

插入语句在执行前不可以确定具体要插入多少条记录(无法预计即将插入记录的数量),比方说使用 INSERT … SELECT、REPLACE … SELECT 或者 LOAD DATA 这种插入语句,一般是使用 AUTO-INC 锁为 AUTO_INCREMENT 修饰的列生成对应的值

  1. 1(Consecutive):表示使用连续模式的自增锁定,在插入数据时,只会对自增长索引的最后一个插入行进行排它锁定,而不是整个表
  2. 2(Interleaved):表示使用交错模式的自增锁定,在插入数据时,会对自增长索引的最后一个插入行进行共享锁定,而不是排它锁定。这允许多个事务并发地插入数据,提高并发性能

使用交错模式时,会导致自增值的顺序会被打乱,虽然提高了事务的并发性,但自增列的值顺序可能会被打乱,因为插入行的锁定顺序可能不是它们实际插入时的顺序
在主从复制场景下时,当 binlog_format 配置为 statement 以语句的方式存储,会造成 slave 同步 master 节点数据回放时产生错乱

在 innodb_autoinc_lock_mode 参数中,传统模式(Traditional)使用排它锁(Exclusive Lock)对整个表进行锁定;连续模式(Consecutive)只对自增长索引的最后一个插入行进行排它锁定;交错模式(Interleaved)则使用共享锁(Shared Lock)对自增长索引的最后一个插入行进行锁定

一般该参数默认值为 1:连续模式,既保证了自增值的顺序性,在插入性能上面又高于 0 传统模式

show variables like 'innodb_autoinc_lock_mode' ;

记录锁

官方类型名称:LOCK_REC_NOT_GAP,记录锁,只对一条记录进行上锁,比方说:

select * from student where id=1 for update;
select * from student where id=1 lock in share mode;

隐私锁定:delete from student where id=1、update student set name = ‘’ where id=1

记录锁也是有 S、X 锁之分的,当一个事务获取了一条记录的 S 锁后,其他事务仍然可以继续获取该记录的 S 锁,但不可以获取该记录的 X 锁;当一个事务获取一条记录的 X 锁后,其他事务既不可以获取该记录的 S 锁,也不可以获取该记录的 X 锁

间隙锁

Gap Lock 为了防止其他事务在一个范围内插入新的记录而引入的一种锁机制。通过生成间隙锁,可以确保其他事务无法在已有记录之间插入新记录,从而维护数据的一致性和完整性

生成间隙锁对于维护数据的一致性、避免幻读等问题非常重要

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第4张图片

会话 vnjohn-transaction1:

begin;
select * from student where id between 4 and 6 for update;

会话 vnjohn-transaction2:

begin;
insert into student values(5,'wangwu');

当会话 1 执行查询使用了索引间隙锁 > 主键索引:4~6,会话 2 插入一条主键为 5 的数据,会被阻塞住,直到会话 1 事务提交,会话 2 才会插入成功

InnoDB 行锁模式及加锁方法

InnoDB 行锁通过给索引上的索引项加锁来实现的,Oracle 是通过在数据块中相应数据行加锁来实现的,而 MySQL 则不同,只有通过索引条件检索数据,InnoDB 才使用行级别锁,否则,InnoDB 会使用表级别锁

在不通过索引条件查询时,InnoDB 使用的是表锁而不是行锁,用以下建表语句举例:

# 建立一张无索引的表
create table tab_no_index(
  id int,
  name varchar(10)
) engine=innodb;
# 插入表数据
insert into tab_no_index values
(1,'1'),
(2,'2'),
(3,'3'),
(4,'4');

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第5张图片

会话 vnjohn-transaction1 会话 vnjohn-transaction2
begin;
select * from tab_no_index where id=1;
begin;
select * from tab_no_index where id=2;
select * from tab_no_index where id=1 for update;
select * from tab_no_index where id=2 for update;

当会话 1 只给其中一行数据加了排它锁,但是会话 2 在请求其他行的排它锁时,会出现锁等待;原因是在没有索引的情况下,InnoDB 只能使用表锁

更新、删除操作无须手动加锁,默认会给数据+上排它锁,一样会出现锁等待。

通过带索引条件查询时,InnoDB 使用的是行锁,用以下建表语句举例:

create table tab_with_index(
  id int,
  name varchar(10)
) engine=innodb;
# 建立 id 列索引
alter table tab_with_index add index id(id);
# 插入数据
insert into tab_with_index values
(1,'1'),
(2,'2'),
(3,'3'),
(4,'4');

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第6张图片

会话 vnjohn-transaction1 会话 vnjohn-transaction2
begin;
select * from tab_with_index where id=1;
begin;
select * from tab_with_index where id=2;
select * from tab_with_index where id=1 for update;
select * from tab_with_index where id=2 for update;

当会话 1 只给其中一行数据加了排它锁,但是会话 2 在请求其他行的排它锁时,不会出现锁等待;原因是在有索引的情况下,InnoDB 会使用行锁

行锁是针对索引加锁,而不是针对记录加锁,仍然以 tab_with_index 表为例,插入一条 id 列相同的数据

insert into tab_with_index  values(1,'4');

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第7张图片

会话 vnjohn-transaction1 会话 vnjohn-transaction2
begin;
select * from tab_with_index where id = 1;
begin;
select * from tab_with_index where id = 1 and name=‘1’ for update;
select * from tab_with_index where id = 1 and name=‘4’ for update;

会话1、会话2 虽然访问是不同行记录,但是使用了相同的索引键,是会出现锁冲突的,所以会话 2 会出现等待锁的情况

死锁

以 student 表为例,演示两个会话之间,在需要互相获取对方的资源情况下,产生的死锁

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第8张图片

会话 vnjohn-transaction1 会话 vnjohn-transaction2
begin;
select * from student where id = 1 for update;
begin;
select * from student where id = 2 for update;
select * from student where id = 2 for update;
select * from student where id = 1 for update;

当执行到第一步 SQL select * from student where id = 2 for update; 时,会看到会话 1 一直会阻塞住,当会话2 执行 SQL select * from student where id = 1 for update; 时,MySQL 检测到了死锁,立即结束了会话2 中事务的执行,此时,会话1 发现原本阻塞的语句立马执行完成了

通过: show engine innodb status\G 命令可以看到死锁的详细情况,一般情况下看不到是哪个事务对那些记录加了什么锁,需要调整系统变量:innodb_status_output_locks(MySQL 5.6.16 引入)缺省值是 OFF

show variables like 'innodb_status_output_locks';
set global innodb_status_output_locks = ON;

解析 MySQL 锁机制:共享锁、排它锁、间隙锁、意向锁等,保障数据安全与高并发的秘密武器_第9张图片

开启以后,再次执行上述语句流程后,执行查看死锁详细情况的命令,效果如上图所示,

MySQL 检测到了死锁的发生,最终争抢锁之下,MySQL 自动回滚了会话2 所在的事务

总结

该篇博文讲解了 MySQL 中各种会发生的锁,包括显式、隐式的锁,共享锁、排它锁、意向锁、自增锁、间隙锁,说明了解决并发事务的问题的两种方案,以及通过间隙锁如何解决可重复读隔离级别下出现幻读的问题,阐述了 InnoDB 存储引擎行锁模式及加锁方法,最后,通过实际的小案例演示了死锁的发生以及如何通过 MySQL 自带的命令查看死锁解决的一个过程。

MySQL 专栏高质量博文如下:

MySQL 内置的监控工具介绍及使用篇

构建优化之城:MySQL 数据建模、数据类型优化与索引常识全面解析

MySQL 数据结构优化与索引细节解析:打造高效数据库的优化秘笈

MySQL 数据访问与查询优化:提升性能的实战策略和解耦优化技巧

深度解析 MySQL 事务、隔离级别和 MVCC 机制:构建高效并发的数据交响乐

MySQL 日志体系解析:保障数据一致性与恢复的三位英雄:Redo Log、Undo Log、Bin Log

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

你可能感兴趣的:(MySQL,mysql,java,数据库)