MySQL锁实战分析!

文章内容收录到个人网站,方便阅读hardyfish.top/

文章内容收录到个人网站,方便阅读hardyfish.top/

what & why

为了保证数据的一致性而设计的面对并发场景的一种规则。

在业务项目中,只要用到了MySQL(默认innoDB引擎),即使只写普通的CRUD,也会涉及到锁。

如果涉及到主动开启数据库事务,了解锁有助于写出效率更高的代码,同时也能避免一些不必要的坑。

除了指导日常生产,如果能通过对锁的了解拓展解决问题的思路,举一反三、合适地应用到其他场景,可以说是不虚此行了。

文章内容收录到个人网站,方便阅读hardyfish.top/

锁分类

乐观锁、悲观锁

是两种思想

乐观锁假定大概率不会出现并发更新冲突,更新数据时通过比较"版本号"来判定是否出现了冲突,如果冲突则处理冲突(放弃执行也可以认为是处理冲突的方式),不冲突则直接执行。

悲观锁假定大概率会出现并发更新冲突,所以需要显式的占有排他锁,期间其他操作者(如果遵守一致的锁协议)看到锁就等待或者放弃执行,直到锁占有方处理完毕。

举例 - 给id为2的货物扣除7个库存

乐观锁实现

sql 复制代码
UPDATE t SET s = s - 7, updatedTime = ? WHERE id = 2 AND s >= 7;

悲观锁实现

sql 复制代码
BEGIN;
SELECT s FROM t WHERE id = 2 FOR UPDATE;
if s < 7
    COMMIT;
    return false
UPDATE t SET s = s - 7, updatedTime = ? WHERE id = 2;
COMMIT;

共享锁、排它锁

共享锁:你锁了我也能锁,也可以叫"读锁","S锁"

  • 锁表(MySQL):LOCK TABLES t READ
  • 解锁:UNLOCK TABLES
  • 锁记录(innoDB):SELECT ... LOCK IN SHARE MODE

排它锁:你锁了我就不能锁了,也可以叫"写锁","X锁"

  • 锁表(MySQL):LOCK TABLES t WRITE
  • 解锁:UNLOCK TABLES
  • 锁记录(innoDB):SELECT ... FOR UPDATE

问题: 如果一个事务先获取了一张表的S锁,还能继续获取X锁么?

表锁、页锁、行锁

按照锁粒度区分的锁。

关于表锁、行锁,对比如表

锁粒度 锁开销 加锁速度 冲突概率 并发度
表锁
行锁

页锁在BDB引擎中有体现,不在本次讨论范围内,简单理解为各种表现都介于表锁和行锁之间

问题:表锁是加在表上的,行锁是加在数据记录上的,那么就存在一个问题,如果想要给表加S锁,难道要检测每一个数据行上有没有X锁么,有没有更快的方式?

意向锁

为了解决不同锁粒度之间的兼容问题,innoDB使用了意向锁。

  • 是表级锁
  • 分为意向共享锁(IS锁)和意向排它锁(IX锁)
  • 想要获取表的S(X)锁,必须先获取IS(IX)锁
  • 想要获取行的S(X)锁,也必须先获取表的IS(IX)锁

问题: 事务A先获取id=1的X锁,之后事务B尝试获取id=2的X锁,都需要获取表的IX锁,会不会冲突?

  • 不会,见:不同锁的兼容性

不同锁的兼容性

在同一张表上,几种锁的兼容性如下

innoDB几种锁模式

演示准备:MySQL 5.7,隔离级别:可重复读

sql 复制代码
CREATE TABLE t (
    id INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    a INT(11) NOT NULL,
    PRIMARY KEY (id),
    KEY (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
INSERT INTO t (id, a) values (1, 1), (5, 5), (10, 10);

查看innoDB状态

如下语句可以显示最近几个事务的状态、查询、写入情况等信息。如果出现了死锁,会显示死锁明细

ini 复制代码
SHOW ENGINE innoDB STATUS;

关于事务和锁的几张表

sql 复制代码
information_schema.innodb_trx        - 当前运行的所有事务
information_schema.innodb_locks      - 已请求但仍未获取到的锁
information_schema.innodb_lock_waits - 锁等待对应关系
 
-- 查看
SELECT * FROM {上面提到的几张表}

记录锁 - Record Locks

锁索引记录

事务A 事务B
BEGIN;UPDATE t SET a = 2 WHERE id = 1;
BEGIN;UPDATE t SET a = 3 WHERE id = 1;
SHOW ENGINE innoDB STATUS;

问题: 怎么理解锁索引记录?多个索引存在的情况下,用哪个索引?都用聚簇索引,还是根据SQL语句的WHERE条件?

事务A 事务B
BEGIN;UPDATE t SET id = 2 WHERE id = 1;
BEGIN;UPDATE t SET a = 2 WHERE a = 1;

现象: 事务B被阻塞

结论: 聚簇索引和二级索引都锁了

间隙锁 - Gap Locks

作用在索引记录之间的间隔,或者索引最小值之前或者最大值之后,不包括索引记录本身

只存在于部分隔离级别中,读提交及更低隔离级别下,不使用间隙锁

间隙锁的存在目的,防止其他事务在间隙中插入记录

间隙锁也分S锁和X锁,且在同一个间隙上,一个事务的S锁和另外一个事务的X锁可以同时存在

使用间隙锁的时机

  • where子句查询条件使用唯一键且使用等于判断时,只有记录锁,没有间隙锁
  • 使用范围判断时,存在间隙锁
  • 非唯一键且使用等于判断 时,锁定索引和索引之前的间隙
事务A 事务B
BEGIN;SELECT * FROM t WHERE a = 5 FOR UPDATE;
BEGIN;INSERT INTO t (id, a) VALUES (2, 2);
SHOW ENGINE innoDB STATUS;

如果事务B插入的记录是(0, 0),执行不会被阻塞

问题: 如果使用非唯一键且使用等于判断时,如果没有命中的记录,加锁情况是怎样的

问题: 如果事务A执行插入(6, 6),未提交时,事务B执行插入(7, 7),事务B会不会被间隙锁阻塞

  • 不会,原因见:插入意向锁

Next-key Locks

记录锁和间隙锁的组合,在下一个索引记录本身和索引之前的间隙加上锁。可以这样理解,间隙锁是左开右开的区间,Next-key锁是左开右闭的区间(除了大于索引最大值的部分)

因此在执行以下语句时,存在的锁是(1, 5)的间隙锁,5的记录锁和(5, 10)的Next-key锁

sql 复制代码
BEGIN;
SELECT * FROM t WHERE a = 5 FOR UPDATE;

插入意向锁 - Insert Intention Locks

一种特殊的间隙锁,意向排它锁,专门为了提高插入的并发度而设计。如果多个事务在同一个索引同一个区间内,插入的位置不冲突,则互相不会阻塞

自增锁 - AUTO-INC Locks

事务执行插入时,体现在自增列上的表级别的锁。虽然是表锁,但是生命周期并不是整个事务,只在SQL执行期间。

挺难演示的,就不演示了,工程上对于自增锁一般不需要投入过多的关注。

不同语句的加锁方式

SELECT ... FROM

不显式指定LOCK IN SHARE MODE或FOR UPDATE时,不加锁

SERIALIZABLE隔离级别下,如果是使用二级索引,则会加S模式的Next-key锁,如果是唯一索引,则只对索引记录加锁,不锁间隙

UPDATE

对搜索遇到的每一条记录上设置X模式的Next-key锁,如果是唯一索引,则不锁间隙

UPDATE的执行可能会导致普通索引的插入或者更新。因此在执行之前,会先进行一个重复索引检查,执行过程中,会对受影响的二级索引使用S锁

DELETE

对搜索遇到的每一条记录上设置X模式的Next-key锁,如果是唯一索引,则不锁间隙

INSERT

获取插入意向锁 → 重复索引检查(S锁) → 如果存在唯一索引且唯一索引没被锁,抛出duplicate key,否则等待锁释放 → 加X记录锁 → 执行插入

死锁

有锁就有死锁的可能性,在工程中要尽量避免死锁出现

必要条件

互斥、请求与保持、不可剥夺、循环等待

可能的场景及对应处理方式

乱序加锁

事务A 事务B
BEGIN;SELECT * FROM t WHERE id = 1 FOR UPDATE;
BEGIN;SELECT * FROM t WHERE id = 5 FOR UPDATE;
SELECT * FROM t WHERE id = 5 FOR UPDATE;→ 阻塞
SELECT * FROM t WHERE id = 1 FOR UPDATE;→ ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

解决方案:在事务最开始,就先申请相关资源的锁,并且以统一的顺序获取。通过破坏循环等待条件实现死锁避免

LOCK IN SHARE MODE引发的死锁

事务A 事务B
BEGIN;SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;
BEGIN;SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;
UPDATE t SET a = 2 WHERE id = 1;→ 阻塞
UPDATE t SET a = 2 WHERE id = 1;→ ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

解决方案:使用X锁(SELECT ... FOR UPDATE)

问题:什么时候使用S锁?

  • 如果有一张主表A和辅助表B,并且B通过A.id对A产生了依赖。当场景只需要更新B(或插入、删除)时,可以先对对应的A.id加S锁
  • 场景比较少,而且还要注意死锁问题,而且使用X锁问题也不大。所以工程中几乎不用LOCK IN SHARE MODE

ROLLBACK引发的死锁

事务A 事务B 事务C
BEGIN;INSERT INTO t VALUES (2, 2);
BEGIN;INSERT INTO t VALUES (2, 2);→ 阻塞
BEGIN;INSERT INTO t VALUES (2, 2);→ 阻塞
ROLLBACK;
OK ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

根据对INSERT语句执行的锁过程的描述,可知,这里是因为事务A给id=2加了X锁之后,事务B、C都给id=2加了S锁。当事务A回滚时,B、C在拥有S锁的情况下都去请求X锁,导致死锁。原理和LOCK IN SHARE MODE引发的死锁是一致的。

解决方案:工程项目中,对于涉及到唯一锁的并发,考虑引入分布式锁来实现

COMMIT引发的死锁

和ROLLBACK引发的死锁类似,事务A的INSERT语句替换成DELETE FROM t WHERE id = 1; 事务B、C的语句把(2, 2)替换成(1, 1)。在事务A执行COMMIT之后,事务B、C其中一个会成功,另外一个会因为触发死锁而被迫回滚。原理与LOCK IN SHARE MODE引发的死锁一致

间隙锁和插入意向锁冲突引发的死锁

事务A 事务B
事务A 事务B
BEGIN;SELECT * FROM t WHERE a = 10 FOR UPDATE;
BEGIN;INSERT INTO t VALUES (7, 7);→ 阻塞
SHOW ENGINE innoDB STATUS;
INSERT INTO t VALUES (7, 7);
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction OK

产生原因留作思考,根据之前的介绍,应该有能力自行分析

总结

  1. 顺序加锁

  2. 事务开始阶段提前申请需要的锁

    1. 如果事务中包含热点数据,可以考虑把热点数据放在事务执行的靠后阶段,使热点数据被锁定的时间更少。与2本身并不冲突,2是作为通用的方式,如果有较高的性能要求,可以以此为依据做精细化处理
  3. 慎用LOCK IN SHARE MODE

  4. 对于唯一键并发,考虑引入分布式锁

  5. 合理设置索引,设置索引之前,把锁表现也纳入考虑范围(一般工程项目中只考虑查询性能即可)

思考题:SELECT ... FOR UPDATE能不能用来实现分布式锁

文章内容收录到个人网站,方便阅读hardyfish.top/

相关推荐
liu_chunhai6 分钟前
设计模式(3)builder
java·开发语言·设计模式
姜学迁14 分钟前
Rust-枚举
开发语言·后端·rust
一般路过糸.24 分钟前
MySQL数据库——索引
数据库·mysql
ya888g42 分钟前
GESP C++四级样题卷
java·c++·算法
【D'accumulation】1 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
小叶学C++1 小时前
【C++】类与对象(下)
java·开发语言·c++
2401_854391081 小时前
高效开发:SpringBoot网上租赁系统实现细节
java·spring boot·后端
Cikiss1 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
wxin_VXbishe1 小时前
springboot合肥师范学院实习实训管理系统-计算机毕业设计源码31290
java·spring boot·python·spring·servlet·django·php
Cikiss1 小时前
微服务实战——平台属性
java·数据库·后端·微服务