引言
在数据库并发控制中,锁机制是保障数据一致性和隔离性的核心手段。MySQL中意向锁、间隙锁以及next-key锁等复杂锁类型,旨在协调表级锁与行级锁之间的关系,防止数据的脏读、不可重复读和幻读现象,尤其是在可重复读隔离级别下发挥关键作用。然而,锁机制在提升数据隔离性的同时,也可能带来死锁风险和写倾斜问题,影响系统的并发性能和业务逻辑正确性。本文将系统梳理MySQL中意向锁与next-key锁的原理及应用,剖析间隙锁导致的死锁现象,并结合实际案例探讨写倾斜问题及其解决方案,帮助读者深入理解数据库锁机制与事务隔离的内在联系。
意向锁
主要作用是协调表锁与行锁之间的互斥关系。意向锁本身之间是不会发生冲突的,与行级别的共享锁(S 锁)和排他锁(X 锁)也不发生冲突。它们主要与表级别的共享锁(S 锁)和排他锁(X 锁)发生冲突,在表级别加锁时提升效率,避免全表扫描来判断是否有行锁存在。
- 意向共享锁(IS, Intention Shared Lock):当事务打算对表中的某些行加共享锁(S 锁)时,必须首先对该表加一个意向共享锁(IS 锁),告诉其他想锁表的事务该表有行数据加了共享锁。
- 意向排他锁(IX, Intention Exclusive Lock):当事务打算对表中的某些行加排他锁(X 锁)时,必须首先对该表加一个意向排他锁(IX 锁),告诉其他想锁表的事务该表有行数据加了排他锁。
next-key Lock的理解
next-key Lock = gap Lock + row Lock,只在可重复读隔离级别存在。
- 对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,只会加行记录锁。如果查询未精准命中,查询了不存在的值,会产生间隙锁。例如:[1, 5, 10]。select * from table where id=3; 会对(1, 5)(左开右开)加上锁。
- 没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。
- 非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近gap间隙锁。例如,某表数据如下,非唯一索引[2、6、9、9、11、15]。要操作非唯一索引列 9 的数据(select * from table where x = 9 for update),gap锁将会锁定的列是(6, 11](左开右闭),该区间内无法插入数据。
- 主键或唯一索引,范围查询时会对查找范围内的间隙上锁,例如,唯一索引[1、5、10、15]。where id>1 and id<10 for update,gap锁会对(1,10)(左开右开),该区间会加锁。
next-key Lock无法解决的幻读
next-key Lock可以解决当前读下的幻读问题,但引入了死锁问题。next-key Lock无法解决当前读隔离级别下的幻读问题。
间隙锁导致的死锁问题
间隙锁之间不互斥,间隙锁和写锁之间是互斥的。
写倾斜问题
当两个或多个并发事务读取相同的数据并基于这些读取的结果执行写操作时,因为这些事务在执行过程中彼此不可见,最终可能导致违反数据一致性或业务逻辑的结果。
举个例子
假设,医院调度系统,每个医生在一天内只能处理最多 10 个病人。
- 事务 A 和事务 B 都读取到医生1的病人数量是 9。
- 事务 A 判断病人数量小于 10,因此增加一个病人,病人数量变为 10,并提交事务。
- 事务 B 仍然认为病人数量是 9(因为它读取时事务 A 尚未提交),也增加一个病人,病人数量变为 11,并提交事务。
#表结构
CREATE TABLE doctor_patients (doctor_id INT,patient_count INT,PRIMARY KEY (doctor_id)
);#事务A
START TRANSACTION;
SELECT patient_count FROM doctor_patients WHERE doctor_id = 1; -- 读取到的病人数量是 9
-- 逻辑判断:如果病人数量小于10,则增加一个病人
UPDATE doctor_patients SET patient_count = patient_count + 1 WHERE doctor_id = 1;
COMMIT;#事务B
START TRANSACTION;
SELECT patient_count FROM doctor_patients WHERE doctor_id = 1; -- 读取到的病人数量也是 9(因为事务 A 尚未提交)
-- 逻辑判断:如果病人数量小于10,则增加一个病人
UPDATE doctor_patients SET patient_count = patient_count + 1 WHERE doctor_id = 1;
COMMIT;
解决办法
- 开启串行化隔离级别,事务A对行数据加共享锁,事务B也可以对行数据加共享锁。事务A修改行数据时发现事务B加了共享锁只能阻塞,事务B修改行数据时发现事务A加了共享锁此时产生了死锁。保证了不会产生锁倾斜问题。
- 读数据时加上for update,保证其他事务无法读取这行数据。
感谢您的阅读!如果文章中有任何问题或不足之处,欢迎及时指出,您的反馈将帮助我不断改进与完善。期待与您共同探讨技术,共同进步!