记录一次mysql死锁

公司在做活动时有一个抽奖发红包的功能,老大告诉我线上日志出现了死锁信息,吓我一跳。

紧急查看代码,问了下线上人数。代码没看出什么问题,几个月了也做过几次活动,也没出现过,线上也就400-500人, 以前几千人上线都过来了。

原因猜测:

  1、mysql压力过大,毕竟开源软件bug也不少;

       2、代码逻辑过于复杂,某个地方写错了;

      3、都是经验不丰富的程序员,使用的spring框架,事务嵌套足有10层,可能是哪里顺序没搞对。

解决方法:

  1、找出线上日志,这个死锁异常大多如下,几百人上来怎么也有几千个请求,这个异常出现的频率并不高,不超过10个,分析是高并发引起的,大多出现在活动开始的10分钟内

  

 2、业务代码有些复杂,一步步分析太慢,定位到引发异常的位置

 1 if(奖品已经发放过 > 0) {            
 2     throw new ...Exception("红包发放已经处理");
 3 }
 4 
 5 Entity record = new Entity();
 6 
 7 do someting...
 8 //保存红包记录
 9 repository.save(record);
10 repository.flush();

网上查询到mysql并发时如果对记录先拿到共享锁,然后再插入时要再去加排它锁时要先等待共享锁释放,猜测是不是这里先count了再insert时出现了死锁

因此新建一张表

CREATE TABLE `gs_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `me_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uk_me` (`me_id`),
  CONSTRAINT `uk_me` FOREIGN KEY (`me_id`) REFERENCES `test_me` (`id`)
)
//先count一下锁住表记录
long
count = gsTestRepository.count(); GsTest n = new GsTest(); n.setName("sds");
//插入记录 gsTestRepository.
save(n); gsTestRepository.flush();

使用jmeter进行压测,1000个线程并发也没出现死锁,郁闷。。。

此时灵光一现,业务表里面还有一个外键字段,在这段代码执行前还有代码对这个外键记录进行了更新,他们都在同一个事务中,猜想是否存在外键锁(对mysql锁机制不熟,只能瞎猜),再新建一个表,原来表里面加一个外键

CREATE TABLE `gs_test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `me_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `uk_me` (`me_id`),
  CONSTRAINT `uk_me` FOREIGN KEY (`me_id`) REFERENCES `test_me` (`id`)
);
CREATE TABLE `test_me` (
  `id` int(255) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
)

service代码改为

TestMe m1 = testMeRepository.findById(1).get();
m1.setName("" + new Random().nextInt());
        this.testMeRepository.saveAndFlush(m1);

        GsTest n = new GsTest();
        n.setName("sds");
        n.setMeId(1);
        this.gsTestRepository.save(n);
        this.gsTestRepository.flush();

再次压测,Deadlock的出现令我泪流满面,自己好聪明。

本以为事情到这里就结束了,然而在saveAndFlush父表的时候应该会加上排它锁,应该会阻塞在这里,后面不应该会死锁,然而线上的日志显示仍然到了下面。

网上找到一个好命令show engine innodb status来找mysql的事务引擎日志,结果发现死锁

 1 LATEST DETECTED DEADLOCK
 2 ------------------------
 3 2019-06-26 17:20:47 0x4ca8
 4 *** (1) TRANSACTION:
 5 TRANSACTION 859473, ACTIVE 0 sec starting index read
 6 mysql tables in use 1, locked 1
 7 LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
 8 MySQL thread id 233, OS thread handle 8584, query id 2889659 localhost 127.0.0.1 root updating
 9 update test_me set name='1971272098' where id=1
10 *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
11 RECORD LOCKS space id 3702 page no 4 n bits 72 index PRIMARY of table `gs`.`test_me` trx id 859473 lock_mode X locks rec but not gap waiting
12 Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
13  0: len 4; hex 80000001; asc     ;;
14  1: len 6; hex 0000000d1d49; asc      I;;
15  2: len 7; hex 02000002511803; asc     Q  ;;
16  3: len 10; hex 2d383631383338363936; asc -861838696;;
17 
18 *** (2) TRANSACTION:
19 TRANSACTION 859472, ACTIVE 0 sec starting index read
20 mysql tables in use 1, locked 1
21 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
22 MySQL thread id 227, OS thread handle 19624, query id 2889667 localhost 127.0.0.1 root updating
23 update test_me set name='669576346' where id=1
24 *** (2) HOLDS THE LOCK(S):
25 RECORD LOCKS space id 3702 page no 4 n bits 72 index PRIMARY of table `gs`.`test_me` trx id 859472 lock mode S locks rec but not gap
26 Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
27  0: len 4; hex 80000001; asc     ;;
28  1: len 6; hex 0000000d1d49; asc      I;;
29  2: len 7; hex 02000002511803; asc     Q  ;;
30  3: len 10; hex 2d383631383338363936; asc -861838696;;
31 
32 *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
33 RECORD LOCKS space id 3702 page no 4 n bits 72 index PRIMARY of table `gs`.`test_me` trx id 859472 lock_mode X locks rec but not gap waiting
34 Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
35  0: len 4; hex 80000001; asc     ;;
36  1: len 6; hex 0000000d1d49; asc      I;;
37  2: len 7; hex 02000002511803; asc     Q  ;;
38  3: len 10; hex 2d383631383338363936; asc -861838696;;
39 
40 *** WE ROLL BACK TRANSACTION (2)
41 ------------
42 TRANSACTIONS
43 ------------

上面出现死锁是在对父表进行更新的时候,那么也就不是外键直接引发的死锁, 分析日志发现是3个事务同时在select后拿到了 X锁,其中一个事务拿到了S锁,但同时要等待其他事务释放X锁,其他事务在获取S锁的时候又要等待这个事务释放S锁,结果出现死锁。

查询线上的这个日志也发现死锁并不是jvm的异常出现位置(insert字表), 而是在外部事务中insert一个父表的时候。

结论分析:

学好mysql锁对并发编程大有裨益,不能完全相信jvm的日志,使用mysql就要学会看mysql的日志,否则只能是无头苍蝇乱撞。

原文地址:https://www.cnblogs.com/shaozhen/p/11090699.html