• 记录一次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的日志,否则只能是无头苍蝇乱撞。

  • 相关阅读:
    CF-478C
    HDU-2074-叠筐
    HDU-2037-今年暑假不AC
    POJ-2785-4 Values whose Sum is 0
    HDU-1160-FatMouse's Speed
    HDU-1297-Children’s Queue
    Redis客户端管理工具的安装及使用
    Redis客户端管理工具,状态监控工具
    memcached可视化客户端工具
    javascript回调函数
  • 原文地址:https://www.cnblogs.com/shaozhen/p/11090699.html
Copyright © 2020-2023  润新知