主键冲突,失败的insert变成共享锁,导致死锁,经典案例随时复现

 INSERT语句变成共享锁原因

        在MySQL中,当我们使用INSERT语句向表中插入数据时,如果插入的数据与已有的数据主键或唯一索引发生冲突,就会产生唯一键冲突错误(Duplicate Key Error)。此时,MySQL会将该操作转换为一个SELECT语句,并且将产生的排他锁(Exclusive Lock)转换为共享锁(Shared Lock)。

        这种转换的原因是为了保证数据的隔离性和一致性。具体来说,当我们使用INSERT语句插入数据时,MySQL会根据数据表的唯一索引来创建一条排他锁,这样可以确保其他事务不能同时修改该数据。但是,当我们插入的数据与已有的数据发生冲突时,我们需要回滚当前事务并进行重试。为了避免出现死锁等问题,MySQL会将排他锁转换为共享锁,这样其他事务就可以读取该数据了,而不会被阻塞或死锁。

        需要注意的是,该操作只是在发生唯一键冲突时才会执行。如果没有冲突,INSERT操作仍然会持有排他锁,以保证数据的完整性和一致性。

        总之,当INSERT语句产生唯一键冲突时,MySQL会将产生的排他锁转换为共享锁,以避免出现死锁等问题,保证数据的隔离性和一致性。

原本是一次生产死锁,本次极简演示,尽可能通俗易懂。

注意隔离级别是rc,读已提交。可通过下列sql设置当前会话隔离级别为rc。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

  本次排查用到mysql8.0 sql 工具

    1.发生死锁时,查看最后一次死锁的日志。

show engine innodb status;

 查出来结果,直接复制到文本中,搜索DEADLOCK,即可找到死锁的日志。  

 2.分析sql时,查看当前系统中所有锁的信息

SELECT ENGINE,ENGINE_TRANSACTION_ID,THREAD_ID,EVENT_ID,OBJECT_SCHEMA,OBJECT_NAME,INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_STATUS,LOCK_DATA 
FROM performance_schema.data_locks;

建表语句

CREATE TABLE `course_2` (
  `cid` bigint NOT NULL AUTO_INCREMENT,
  `cname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` bigint NOT NULL,
  `cstatus` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`cid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1666491309009006594 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

提前插入一条数据。cid=1。

INSERT INTO coursedb.course_2 (cid, cname, user_id, cstatus) 
VALUES(1, 'java', 1001, '1');

insert本是排他锁,现在我们证明下插入主键冲突会降级为共享锁。证明直接执行两个cid一致insert,就会发现后执行的insert被阻塞即可证明。

操作1,证明 insert 插入主键冲突会降级为共享锁

事务1 依次执行

begin;
INSERT INTO course_2 (cid, cname, user_id, cstatus) 
VALUES(1, 'java', 1001, '1');

此时报错

此时再开个窗口执行

SELECT ENGINE,ENGINE_TRANSACTION_ID,THREAD_ID,EVENT_ID,OBJECT_SCHEMA,OBJECT_NAME,INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_STATUS,LOCK_DATA FROM performance_schema.data_locks;

可以看到insert由于主键冲突变成了共享锁。而且第一个记录,意向写锁(IX锁),更能证明,本来是排它锁(X),然后失败变共享锁(S)了。

为了达到更清晰的认识,可以继续走下面步骤

 事务2 依次执行

begin;
select * from course_2  where cid=1 lock in share mode ;
select * from course_2  where cid=1 for update;

当执行到共享锁,能查到cid=1的数据,执行到for update会被事务1,阻塞。

因为共享锁可以正常执行,排它锁阻塞,所以判定事务1获取的是共享锁。

然后事务1 执行

commit;

提交事务后,事务2 for update能正常查询。

注意最后事务2也commit下。释放排他锁。

事务2 执行

commit;

 操作2,利用共享锁制造死锁

事务1 依次执行

begin;
INSERT INTO course_2 (cid, cname, user_id, cstatus) 
VALUES(1, 'java', 1001, '1');

此时制造了锁住cid=1记录的共享锁。

事务2 一次执行

begin;
select * from course_2  where cid=1 for update;

事务2获取排它锁,因为事务1持有共享锁,阻塞中。

事务1 执行

select * from course_2  where cid=1 for update;

事务1 持有共享锁,尝试获取排他锁。

此时看事务2,报死锁。

 死锁产生了。

这是可以用下面sql查看死锁日志。

show engine innodb status;

死锁原理

          当事务1获取到共享锁。这时,事务2执行"select * from course_2 where cid=1 for update"语句来获取该行的排他锁,就会被阻塞,因为该行已经被事务1持有了共享锁。而当事务2持有该行的排他锁时,如果事务1再执行"select * from course_2 where cid=1 for update"语句来获取该行的排他锁,也会被阻塞,因为该行已经被事务2持有了排他锁。
这样,事务1和事务2就相互等待对方释放所持有的锁,形成了死锁。如果不采取相应的措施来解除死锁,这两个事务就会一直被阻塞,无法继续执行,从而导致系统性能下降和数据不一致等问题的发生。不过我的数据库开启了死锁检测。所以直接回滚了事务2.

推荐锁的基础知识

MySQL(十三):小一万字+14张图读懂锁机制_星河之码的博客-CSDN博客


 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
由于ThreadLocal只在当前线程内起作用,因此在线程池等场景下使用时需要特别注意,避免ThreadLocal导致问题。 以下是一个示例代码,该代码使用了ThreadLocal来保存线程特有的对象,同时在使用线程池时,由于线程重用导致ThreadLocal中的对象无法正确释放,最终导致系统。 ``` public class ThreadLocalDeadLockDemo { static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10), r -> { Thread t = new Thread(r); t.setDaemon(true); return t; }); static ThreadLocal<Map<String, String>> threadLocal = new ThreadLocal<Map<String,String>>(){ protected Map<String, String> initialValue(){ return new HashMap<>(); } }; public static void main(String[] args) throws Exception { //提交一个任务,该任务会占用ThreadLocal中的对象 executor.execute(() -> { threadLocal.get().put("test", "value"); //模拟业务逻辑执行的过程 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } threadLocal.get().remove("test"); }); //提交一个任务,会尝试获取ThreadLocal对象 executor.execute(() -> { Map<String, String> map = threadLocal.get(); //模拟对ThreadLocal对象的操作,由于上一个任务还未释放对象,导致无法获取到对象,进而导致线程 System.out.println(map.get("test")); }); executor.shutdown(); } } ``` 当运行该程序时,会发程序陷入状态,输出结果停留在“value”处,无法继续执行。 因此,在使用ThreadLocal时,需要慎重考虑其使用场景,避免出等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欣辰守护者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值