MySQL_1:数据库悲观锁

目录

一、悲观锁

1、临键锁获取策略

2、临键锁退化策略

1.如果走的是主键/唯一索引的等值匹配,临键锁退化成记录锁或者间隙锁。

2.如果走的是普通索引的等值匹配,那么会对查到的数据行加锁,并且会向后推导出间隙锁

3.如果走的是非索引,那么整个表都会被加锁(严重)

4、范围查询时临键锁不退化,锁区间合并

二、总结


我们的业务通常放在一个事务中,开启事务start transaction/ begin ,提交事务commit ,回滚事务rollback。

以及查看加锁情况:

sql 复制代码
SELECT
  ENGINE,
  ENGINE_TRANSACTION_ID,
  OBJECT_NAME,
  INDEX_NAME,
  LOCK_TYPE,
  LOCK_MODE,
  LOCK_DATA
FROM performance_schema.data_locks

之后的演示中,我们重点观察的是输出Lock_Type,Lock_Mode和Lock_Data.

一、悲观锁

当形如SELECT ... FORM ... WHERE ... FOR UPDATE. 的SQL被执行时,数据库做了两件事情:1.select 2.给查询到的数据行(整行数据)上锁 ,在本事务结束之前这些数据行不能被其他事务写入

只有等到当前事务commit 或者 rolback后,锁才会释放,其他事务才能加锁。

但是需要注意的是,查询是否走索引会影响锁定多少数据行,还与我们的数据库事务级别有关。

先查看数据库事务级别,MySQL默认为REPEATABLE-READ 可重复读。

sql 复制代码
select @@transaction_isolation;

我们当前事务默认级别为可重复读,其他级别得事务可以总结如下

隔离级别 中文名称 脏读 不可重复读 幻读 (InnoDB) 核心特点
READ UNCOMMITTED 读未提交 存在 存在 存在 几乎不用,能读到别的事务没提交的数据,问题最多
READ COMMITTED 读已提交 (RC) 存在 存在 读到其他事务已经提交的数据;没有间隙锁,只有行锁;线上很多业务会改成这个级别
REPEATABLE READ 可重复读 (RR,MySQL 默认) 无 (临键锁解决) 同一个事务多次读取,结果保持一致;存在行锁、间隙锁、临键锁 Next‑Key Lock
SERIALIZABLE 串行化 最高级别,所有普通 select 隐式转为select ... lock in share mode,全部加共享锁;完全串行执行,并发性能很差

for update会生成一个临键锁 = 间隙锁 + 记录锁

临键锁是一个左开右闭的区间锁,例如(a,b] 表示,a和b之间的空隙内无法插入新数据,b无法被修改。a可以被被修改。

什么是间隙锁?

例如表中有如下排序后的索引:

1,2,4,5,7,10

那么就有如下间隙:

(-∞,1)(1,2)(2,4)(4,5)(5,7)(7,10)(10,+∞)这7个开区间。

如果加间隙锁,就是把这些区间给锁住,禁止在此区间内插入数据。

只有该事务提交或者回滚,才会释放锁。

1、临键锁获取策略

索引 B + 树定位(PAGE_CUR_GE,找第一条 ≥目标值 的索引记录),这个值就是右边界。

在B+树索引中,这条索引天然存着上一条索引的索引值。这个值就作为左边界。

例如,有如下主键排序为:1,2,3,4,5,7

where条件中id = 3,那么拿到临键锁(2,3】

where条件中id = 6,那么拿到临键锁(5,7】

2、临键锁退化策略

为了演示,先建立如下表:

sql 复制代码
create table test (
  id int primary key AUTO_INCREMENT,
  b varchar(10) not null unique,
  c varchar(10) not null,
  d varchar(10) not null,
  index(c)
);

id是主键,b是唯一索引,c是普通索引,d是普通字段。

插入原始数据:

1.如果走的是主键/唯一索引的等值匹配,临键锁退化成记录锁或者间隙锁。

sql 复制代码
select * from test where id = 3 for update;   --id为主键/唯一索引

id = 3,先拿到临键锁(1,3】,查到有记录 ,则临键锁退化成记录锁,id = 3数据行被加锁

加锁情况:

当我们执行for update时会有一个IX,IX 锁(Intention Exclusive Lock,意向排他锁) 是 InnoDB 中的一种表级意向锁 。它的核心作用是:**表明一个事务"有意向"在表中的某些行上加排他锁(X锁)。**所以我们不用关心。

第二行,我们的Lock_Type是RECORD行锁,同时锁模式为X排他锁和非间隙锁。Not_Gap,这就意味着临建锁退化成记录锁,与我们分析一致。

sql 复制代码
select * from test where id = 6 for update;   --id为主键/唯一索引

id = 6,先拿到临键锁(5,7】,没有匹配到id = 6的记录 ,那么丢弃右端点,退化为间隙锁(5,7)

数据行被锁住,锁模式为间隙锁,右端点为7,左端点未知。这里需要自己推到出左端点,我们预期是5,那么就是(5,7)。为了验证,我们插入一条id = 6的数据,看是否可以成功。

可以发现,这个事务进入锁等待状态,几秒钟过后,事务锁等待超时而回滚。

与我们推导一致。

sql 复制代码
select * from test where id in (1,2,5,6) for update;    -- 同样的如果,id = 5 的记录不存在,则加间隙锁。

再例如这句SQL,本质是多次等值查询,每次查询都用不同的锁对象。

查id = 1,5时临键锁退化成两个记录锁(因为可以查到),查id = 2,6时匹配不到,那么临键锁退化成两个间隙锁(1,3)、(5,7).

因为不是范围查询,所以区间不可以合并。

2.如果走的是普通索引的等值匹配,那么会对查到的数据行加锁,并且会向后推导出间隙锁

c列是普通索引,我们适当增添数据如下:

c列排序后如下:

A、B1、B2、C、D、E

因为普通索引它没有进行唯一校验,需要一直向后方去找间隙,好的一点是索引已经排序好了,只需要找出第一个不同的索引值即可作为新间隙锁的右区间。

sql 复制代码
select * from test where c = 'B' for update;    -- c字段为普通索引

首先还是会拿到临键锁(A,B1】,命中数据,因此临键锁退化成记录锁,B1数据行加锁。

但是还没有结束,需要继续向后找。

B2命中,加入记录锁中。一直到C,未命中停止查找。将(B2,C)最为新的间隙锁。

整个过程,临键锁只退化了一次,生成了一个新的间隙锁和两个新的记录锁。

首先B1的id为3,B2的id为6的二级索引条目加上锁。

其次,c列值为'C',id = 5的间隙锁也生成了,与我们推断一致。

但是多了最后两条记录,为什么?因为要回表,B+树中普通索引除了存自己的索引键值等信息外,还会存主键值,需要拿到主键值给整个数据行加锁,这就需要回表操作。

因此多了两条记录,最后这两条记录才是数据行记录锁。

我们适当增加数据:

c列索引排序后:A,B1,B2,C,D,E,G

sql 复制代码
select * from test where c = 'F' for update;  -- c为普通索引

先拿到临键锁(E,G】,匹配不到F,因此右端点丢弃,临键锁退化为(E,G)间隙锁。

这与们推导一致。

3.如果走的是非索引,那么整个表都会被加锁(严重)

sql 复制代码
select * from test where d = 'data2' for update;  -- d字段为非索引字段

所有数据都被加锁了,而且还有一个虚拟上限+∞出现,更加说明整个表都被锁住。

此时无法往表中写入任何数据,造成很严重的后果。

sql 复制代码
select * from test where d = 'data8' for update;    --d为非索引字段

可以发现,无论是否命中数据,都会给整个表加锁。

4、范围查询时临键锁不退化,锁区间合并

sql 复制代码
select * from test where id > 3 for update; -- id为主键

先拿到临键锁(3,5】,不退化,然后拿到(5,6】,(6,7】(7,10】,(10,11】,(11,=∞】

这个过程临键锁不退化,整个过程用的都是同意把锁,因此可以合并区间。

整体锁区间合并为(3,+∞】

这个时候从id = 5开始全部加的都是排他锁,(3,+∞】内不允许插入数据。

例如:

最后只能超时失败:

如果条件改成id>=3,那么锁区间为(1,+∞】,也需要能快速知道为什么。

类似的范围查询:>= ,<= ,> ,< ,between ... and ...

要注意between...and...拆分成>= and <= ,是等价的。都会进行一次扫描,得到的锁区间一致。

此时临键锁不退化,最终得到合并的锁区间:(1,7】

如果拆开的话,

仍然得到一样的锁区间(1,7】

二、总结

以上就是 MySQL 悲观锁机制的详细内容,使用悲观锁会阻塞住其他事务,在并发情况下,可能会导致数据库锁冲突增多、事务排队等待,严重时引发死锁,整体并发性能下降

悲观锁基于当前读 实现,依靠行锁、间隙锁、临键锁来防止幻读;for update / lock in share mode、update、delete 都会触发当前读

如果索引失效,会退化为表锁行为,给全表已有行加排他记录锁,大量事务被阻塞;

事务持有锁期间其他事务需要锁等待,等待超时会抛出lock wait timeout

多个事务互相持有对方需要的锁,就会产生死锁,MySQL 会主动回滚代价较小的事务来解除死锁;

锁的粒度越大(范围查询、全表扫描),锁住的记录与间隙越多,并发能力越差。

高并发场景优先考虑 MVCC 快照读,或者改用乐观锁(版本号机制),减少锁竞争。禁止悲观锁走非索引字段,避免全表扫描。

相关推荐
会编程的吕洞宾1 小时前
Spring AI 2.0 接 Milvus 做混合检索:RAG 召回率翻倍的实战方案
人工智能·spring·milvus
Lost of 程序猿1 小时前
ASP.NET Core SignalR 实战:从实时推送到底层协议与高可用部署
后端·asp.net
QCodingDev1 小时前
Spring AI Alibaba Graph实战:从ReAct Agent到Workflow,企业AI复杂流程该如何编排?
java·人工智能·spring·ai·ai编程·企业ai
JasmineWr1 小时前
Spring BeanDefinition 与 Bean 生命周期、循环依赖解析
java·后端·spring
QQ_21696290961 小时前
【项目编号:project86564】SpringBoot供应链管理系统:采购、供应商、库存、销售、统计报表一体化实战
java·spring boot·后端
灯澜忆梦2 小时前
【基于GO的Web开发10】gin获取HTML-Form表单提交参数
前端·后端·golang·html·gin
Java牛马10 小时前
SpringBoot Starter 依赖相关总结
java·spring·springboot·starter·自动装配·依赖
吴声子夜歌10 小时前
Java面试——Spring Cloud原理及应用(二)
java·spring cloud·面试
SomeB1oody10 小时前
【RustyML入门】6.3. 并行归约
开发语言·后端·机器学习·rust·教程