服了!DELETE 同一行记录也会造成死锁!!
作者:转转技术团队
链接:https://juejin.cn/post/7387227689319563290
来源:稀土掘金
MySQL 锁回顾
共享锁
使用共享锁(Shared Lock)时,它允许持有该锁的事务读取一行数据,但不允许对其进行更新或删除。这种锁的主要目的是防止其他事务修改被锁定的数据。
举个例子:共享锁(Shared Lock) 就像是大家一起看电视,但只能看,不能改变频道。这意味着多个事务可以同时读取同一份数据,但不能修改它。共享锁适用于只读操作,比如查看账户余额或者商品库存。
sql
CREATE TABLE employees (
emp_no INT NOT NULL,
birth_date DATE NOT NULL,
first_name VARCHAR(14) NOT NULL,
last_name VARCHAR(16) NOT NULL,
gender ENUM('M','F') NOT NULL,
hire_date DATE NOT NULL,
middle_name VARCHAR(14),
PRIMARY KEY (emp_no)
);
INSERT INTO `employees` VALUES (101, '1990-01-15', 'John', 'Doe', 'M', '2020-05-10', 'Michael', 2000.00);
INSERT INTO `employees` VALUES (102, '1988-03-20', 'Jane', 'Smith', 'F', '2019-12-01', 'Elizabeth', 1500.00);
INSERT INTO `employees` VALUES (103, '1995-07-05', 'Robert', 'Johnson', 'M', '2021-02-18', 'William', 1800.00);
-- 在事务1中获取共享锁
START TRANSACTION;
SELECT salary FROM employees WHERE emp_no = 101 LOCK IN SHARE MODE;
-- 此时其他事务可以读取 salary,但不能修改或删除
-- 在事务2中尝试修改 salary(会被阻塞)
UPDATE employees SET salary = 6000 WHERE emp_no = 101;
-- 这里会等待,直到事务1释放共享锁
-- 在事务1中继续操作或提交
COMMIT;
共享锁,启动两个事务,事务1启动后,可以正常看到具体的数值:
sql
BEGIN;
SELECT salary FROM employees WHERE emp_no = 101 LOCK IN SHARE MODE;
--COMMIT;
新启动一个事务查询同一行,发现可以正常查询:
排他锁(Exclusive Lock) 就像是你独占一台电视,可以随便换频道,甚至关掉。其他人想看电视,需要你交出电视控制权才可以,否则看都不能看。排他锁用于数据修改操作,比如更新账户余额、添加新订单或者删除记录。只有一个事务能持有排他锁,其他事务必须等待它释放锁后才能操作。
事务1,可以看到查询结果,但没有提交,所以事务无法结束。
事务2,无法看到查询结果,因为事务1独占排他锁(事务2模拟的时候不要执行COMMIT):
sql
BEGIN;
SELECT salary FROM employees WHERE emp_no = 101 FOR UPDATE;
COMMIT;
提交事务1后,事务2的查询结果才能看到:
表锁:
意向锁 :意向锁分为意向共享锁和意向排它锁,只要搞清楚一点,这两个锁瞬间就懂了,这一点就是:标准锁和排他锁不可以同时添加!简单说就是,当多个事务都进行了共享锁请求,系统会查看每一个事务请求共享锁的数据是不是已经有了排他锁,如果有,就不再添加共享锁了。如果没有,注意是事务中请求的表中的所有数据都没有添加排他锁,就会被标记,这个标记就叫做意向共享锁,实际执行的时候这些被标记的意向共享锁就会转化为共享锁。
演示:事务A为ID为101的数据添加了排他锁(注意执行的内容是选中内容,没有执行COMMIT!)
sql
BEGIN;
SELECT salary FROM employees WHERE emp_no = 101 FOR UPDATE;
COMMIT;
事务B为id为101和102的数据请求共享锁,因为事务A中已经为101请求了排它锁,所以事务B中的共享锁请求失败,被阻塞,需要等待事务A提交之后才会执行。
sql
BEGIN;
SELECT salary FROM employees WHERE emp_no = 101 or emp_no = 102 LOCK IN SHARE MODE;
COMMIT;
事务A提交之后,事务B的数据才会被查询出来,按理说如果添加了共享锁,是可以直接查询出结果的:
自增锁 :就是mysql中的自增,表中如果id字段是自增字段,为了保证其自增id的连续性,其中一个事务插入数据的时候,另一个事务插入数据需要等待前一个事务提交完成才可以继续。这个没什么好演示的。
行锁:
记录锁:和排它锁类似,锁会加载包含索引的数据行中,简单说就是通过索引来记录的排它锁,通过索引的辅助,可以避免添加锁需要扫描全表。
演示,注意记录锁需要索引,例子中的mytable表中的id为自增主键,即为索引:
sql
CREATE TABLE mytable (
id INT AUTO_INCREMENT PRIMARY KEY,
value VARCHAR(20)
);
INSERT INTO mytable (value) VALUES ('A');
INSERT INTO mytable (value) VALUES ('B');
INSERT INTO mytable (value) VALUES ('C');
事务A执行了查询id=1的数据,value=A,此时没有commit;事务没有提交。
事务B对id=1的value进行更新操作,把原来的值'A'更新为'X',执行并commit提交
此时由于事务A拿着记录锁,事务B更新操作被阻塞,所以数据库中的数据依然是'A'
提交事务A
因为事务A已经提交,记录锁被释放,数据库中的数据被更新了。
间隙锁
主要目的是同一个事务中同样的查询不能有不同的结果:
演示:事务A查询id小于5的数据,其实数据库中只有三条数据
事务B插入第四条数据,但事务A已经占有了间隙锁,事务B执行被阻塞。如果此时不阻塞,则事务A如果多次相同查询会造成幻读。
所以我们会看到,数据库依然只有三条数据:
事务A提交,释放间隙锁
数据库数据更新为4条
临键锁
记录锁是为了保持数据一致性,会锁定自身,保证查到的数据在同一个事务里保持一致。间隙锁是为了避免幻读,保证一个事务中多次查询同样的数据结果是一样的,临键锁就是记录锁+间隙锁。
DELETE 流程
在深入分析问题原因之前先对 DELETE 操作的基本流程进行复习。众所周知,MySQL 以页作为数据的基本存储单位,每个页内包含两个主要的链表:正常记录链表和垃圾链表。每条记录都有一个记录头,记录头中包括一个关键属性------deleted_flag。
执行 DELETE 操作期间,系统首先将正常记录的记录头中的 delete_flag 标记设置为 1。这一步骤也被称为 delete mark,是数据删除流程的一部分。
在事务成功提交之后,由 purge 线程 负责对已标记为删除的数据执行逻辑删除操作。这一过程包括将记录从正常记录链表中移除,并将它们添加到垃圾链表中,以便后续的清理工作。
针对不同状态下的记录,MySQL 在加锁时采取不同的策略,特别是在处理唯一索引上记录的加锁情况。以下是具体的加锁规则:
- 正常记录: 对于未被标记为删除的记录,MySQL 会施加记录锁,以确保事务的隔离性和数据的一致性。
- delete mark: 当记录已被标记为删除(即 delete_flag 被设置为1),但尚未由 purge 线程清理时,MySQL 会对这些记录施加临键锁,以避免在清理前发生数据冲突。
- 已删除记录: 对于已经被 purge 线程逻辑删除的记录,MySQL 会施加间隙锁,这允许在已删除记录的索引位置插入新记录,同时保持索引的完整性和顺序性。
原因
会对这些记录施加临键锁,以避免在清理前发生数据冲突。
- 已删除记录: 对于已经被 purge 线程逻辑删除的记录,MySQL 会施加间隙锁,这允许在已删除记录的索引位置插入新记录,同时保持索引的完整性和顺序性。
最终原作者采用了分布式锁的方案解决了问题,我个人认为是否查看了mysql的锁超时时间呢?是不是锁超时时间设置的太短了呢?