关于死锁:故障分析-MySQL死锁案例分析

作者:杨奇龙 网名“北在北方”,资深 DBA,次要负责数据库架构设计和运维平台开发工作,善于数据库性能调优、故障诊断。 本文起源:原创投稿 *爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。 一 背景死锁,其实是一个很有意思也很有挑战的技术问题,大略每个DBA和局部开发同学都会在工作过程中遇见 。 本次分享的一个死锁案例是 波及通过辅助索引的更新以及通过主键删除导致的死锁。心愿可能对想理解死锁的敌人有所帮忙。 二 案例剖析2.1 业务逻辑select for update 表记录并加上 x 锁,查问数据,做业务逻辑解决,而后删除该记录。还有其余业务逻辑要更新记录,导致死锁。 2.2 环境阐明数据库 MySQL 8.0.30 事务隔离级别 REPEATABLE-READ create table dl(id int auto_increment primary key,c1 int not null ,c2 int not null,key idx_c1(c1));insert into dl(c1,c2) values (3,1),(3,2),(3,2),(3,3),(4,4),(5,5);2.3 测试用例 2.4 死锁日志------------------------LATEST DETECTED DEADLOCK------------------------2022-12-03 16:43:59 140261132850944*** (1) TRANSACTION:TRANSACTION 1416764, ACTIVE 15 sec starting index readmysql tables in use 1, locked 1LOCK WAIT 5 lock struct(s), heap size 1128, 3 row lock(s)MySQL thread id 15, OS thread handle 140261086668544, query id 283 localhost msandbox updatingupdate dl set c2=10 where c1=5*** (1) HOLDS THE LOCK(S):RECORD LOCKS space id 49 page no 5 n bits 80 index idx_c1 of table `test`.`dl` trx id 1416764 lock_mode XRecord lock, heap no 8 PHYSICAL RECORD: n_fields 2; compact format; info bits 0*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 49 page no 4 n bits 80 index PRIMARY of table `test`.`dl` trx id 1416764 lock_mode X locks rec but not gap waitingRecord lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 32*** (2) TRANSACTION:TRANSACTION 1416759, ACTIVE 23 sec updating or deletingmysql tables in use 1, locked 1LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1MySQL thread id 16, OS thread handle 140261085611776, query id 286 localhost msandbox updatingdelete from dl where id=6*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 49 page no 4 n bits 80 index PRIMARY of table `test`.`dl` trx id 1416759 lock_mode X locks rec but not gapRecord lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 32*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 49 page no 5 n bits 80 index idx_c1 of table `test`.`dl` trx id 1416759 lock_mode X locks rec but not gap waitingRecord lock, heap no 8 PHYSICAL RECORD: n_fields 2; compact format; info bits 0*** WE ROLL BACK TRANSACTION (2)2.5 死锁剖析sess1 开启一个事务,在T2 时刻执行 select for update,持有id=6的lock_mode X record lock.sess2 在T3 时刻执行依据c1=5的更新,然而其加锁程序是先在索引idx_c1上加锁,顺利加锁,而后到申请加主键上加id=6的锁,发现sess1曾经持有主键 id=6 的X的锁,因而须要期待。如日志中 (1) TRANSACTION: 中 WAITING FOR的提醒 RECORD LOCKS space id 49 page no 4 n bits 80 index PRIMARY of table test.dl trx id 1416764 lock_mode X locks rec but not gap waitingsess1 执行 delete id=6 的操作,因为事务自身曾经持有了主键上的锁,删除记录同时要对索引idx_c1上的记录加上 lock_mode X record lock,发现该锁曾经被sess2持有,造成了死锁条件,sess1 报错,产生回滚。2.6 如何解决本文中死锁的起因是因为 sess2 通过辅助索引进行更新,因而举荐的防止死锁计划是把sess2 应用辅助索引的更新改成基于主键进行更新,从而防止申请idx_c1上的加锁造成循环期待产生死锁。 ...

January 5, 2023 · 2 min · jiezi

关于死锁:新特性解读-MySQL-80死锁日志改进

作者:胡呈清 爱可生 DBA 团队成员,善于故障剖析、性能优化,集体博客:https://www.jianshu.com/u/a95...,欢送探讨。 本文起源:原创投稿 *爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。 重要改良MySQL8.0 的死锁日志能够看到事务1持有的锁信息了: 这对咱们剖析死锁无疑是个很好的帮忙,而在 MySQL5.7 是没有这个信息的,始终饱受诟病: 注意事项然而这在某些状况下可能会产生一些误会,比方事务1持有锁和期待锁是同一个锁: 为什么会呈现这种状况?这是不是bug?必须不是bug,咱们来复现这一死锁场景: ##设置RC隔离级别CREATE TABLE `t2` ( `c1` int(11) NOT NULL, `c2` int(11) DEFAULT NULL, PRIMARY KEY (`c1`), UNIQUE KEY `c2` (`c2`));insert into t2 values(1,1),(5,4),(20,20); 死锁逻辑: session2 插入胜利,对 c2 索引 10 这一记录加 X Lock,即死锁日志中的lock_mode X locks rec but not gap;session1 插入时,产生惟一键抵触,须要对 c2 索引 10 这一记录加 S Lock,带 gap 属性,即锁的范畴为 (4,10]。然而因为 session2 曾经对记录加了 X Lock,与 S Lock 互斥,所以必须期待 session 2 先开释锁,也就是死锁日志中的lock mode S waiting;session2 再次插入 9,在 (4,10] 范畴内,这个地位有 session1 的 gap 锁(尽管还在锁队列中,没有加上),插入意向锁会被 gap 锁阻塞,即死锁日志中的 lock_mode X locks gap before rec insert intention waiting。session1、session2 相互期待,所以造成死锁。session1 期待获取的锁 S Lock 阻塞了 session2 将要获取的锁,这在 MySQL8.0 中就会显示成 session1 持有的锁,同时也是 session1 期待的锁。就是这样。 ...

December 23, 2021 · 1 min · jiezi

关于死锁:LiteOSSpinLock自旋锁及LockDep死锁检测

摘要:除了多核的自旋锁机制,本文会介绍下LiteOS 5.0引入的LockDep死锁检测个性。2020年12月公布的LiteOS 5.0推出了全新的内核,反对SMP多核调度性能。想学习SMP多核调度性能,须要理解下SpinLock自旋锁。除了多核的自旋锁机制,本文还会介绍下LiteOS 5.0引入的LockDep死锁检测个性。 本文中所波及的LiteOS源码,均能够在LiteOS开源站点https://gitee.com/LiteOS/LiteOS 获取。 自旋锁SpinLock源代码、开发文档,LockDep死锁检测个性代码文档列表如下: kernelincludelos_spinlock.h 自旋锁头文件网页获取自旋锁源码 https://gitee.com/LiteOS/Lite...。 spinlock.S、arch/spinlock.h 自旋锁汇编代码文件及头文件 针对不同的CPU架构,有两套代码。因为自旋锁实用于多核,M核架构archarmcortex_m下不蕴含自旋锁的汇编文件。如下: archarmcortex_a_r架构 汇编代码文件https://gitee.com/LiteOS/Lite...。头文件https://gitee.com/LiteOS/Lite...。archarm64架构 汇编代码文件 https://gitee.com/LiteOS/Lite...。头文件https://gitee.com/LiteOS/Lite...。开发指南自旋锁文档在线文档https://gitee.com/LiteOS/Lite...。 LockDep死锁检测 死锁检测代码蕴含: 头文件https://gitee.com/LiteOS/Lite...C代码文件https://gitee.com/LiteOS/Lite...。咱们首先来看看自旋锁。 1、SpinLock 自旋锁在多核环境中,因为应用雷同的内存空间,存在对同一资源进行拜访的状况,所以须要互斥拜访机制来保障同一时刻只有一个核进行操作。自旋锁就是这样的一种机制。 自旋锁是指当一个线程在获取锁时,如果锁曾经被其它线程获取,那么该线程将循环期待,并一直判断是否可能胜利获取锁,直到获取到锁才会退出循环。因而倡议爱护耗时较短的操作,避免对系统整体性能有显著的影响。 自旋锁与互斥锁比拟相似,它们都是为了解决对共享资源的互斥应用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。然而两者在调度机制上略有不同,对于互斥锁,如果锁曾经被占用,锁申请者会被阻塞;然而自旋锁不会引起调用者阻塞,会始终循环检测自旋锁是否曾经被开释。自旋锁用于多核不同CPU核查资源的互斥拜访,互斥锁用于同一CPU核内不同工作对资源的互斥拜访。 自旋锁SpinLock外围的代码都在kernelincludelos_spinlock.h头文件中,蕴含struct Spinlock构造体定义、一些inline内联函数LOS_SpinXXX,还有一些LockDep死锁检测相干的宏定义LOCKDEP_XXXX。 1.1 Spinlock 自旋锁构造体自旋锁构造体Spinlock定义如下,次要的成员变量为size_t rawLock,这是自旋锁是否占用持有的胜利的标记:为0时,锁没有被持有,为1时示意被胜利持有。当开启LockDep死锁检测调测个性时,会使能另外3个成员变量,记录持有自旋锁的CPU核信息、工作信息。 struct Spinlock { size_t rawLock; /**< 原始自旋锁 */#ifdef LOSCFG_KERNEL_SMP_LOCKDEP UINT32 cpuid; /**< 死锁检测个性开启时,持有自旋锁的CPU核 */ VOID *owner; /**< 死锁检测个性开启时,持有自旋锁的工作的TCB指针 */ const CHAR *name; /**< 死锁检测个性开启时,持有自旋锁的工作的名称 */#endif};1.2 Spinlock 自旋锁罕用函数接口LiteOS自旋锁模块为用户提供上面几种性能,蕴含自旋锁初始化,申请/开释,查问自旋锁状态等。自旋锁相干的函数、宏定义只反对SMP - Symmetric MultiProcessor模式,当单核UP - UniProcessor时,函数不失效。接口详细信息能够查看API参考。 1.2.1 自旋锁初始化自旋锁初始化的内联函数如下,其中参数SPIN_LOCK_S *lock,即自旋锁构造体指针,其中SPIN_LOCK_S是Spinlock的typedef别名,在kernelincludelos_lockdep.h文件中定义的。 自旋锁初始时,会把自旋锁标记为0:lock->rawLock = 0,当开启死锁检测个性时,也会做相应的初始化。 LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinInit(SPIN_LOCK_S *lock){ lock->rawLock = 0;#ifdef LOSCFG_KERNEL_SMP_LOCKDEP lock->cpuid = (UINT32)-1; lock->owner = SPINLOCK_OWNER_INIT; lock->name = "spinlock";#endif}LOS_SpinInit()是动静初始化的自旋锁,LiteOS还提供了动态初始化自旋锁的办法SPIN_LOCK_INIT(lock): ...

February 27, 2021 · 4 min · jiezi

关于死锁:故障分析-全局读锁一直没有释放发生了什么

作者:刘开洋爱可生交付服务部团队北京 DBA,次要负责解决 MySQL 的 troubleshooting 和我司自研数据库自动化治理平台 DMP 的日常运维问题,对数据库及周边技术有浓重的学习趣味,喜爱看书,谋求技术。本文起源:原创投稿* 爱可生开源社区出品,原创内容未经受权不得随便应用,转载请分割小编并注明起源。问题:在一个客户的线上监控告警中,提醒主从提早一直升高,咱们登上数据库进行查看一下,发现 MySQL 从库复制状态提醒 SQL 线程在 waiting for global read lock。 在数据库的过程列表中发现了存在的期待全局读锁和 kill slave 的过程;高可用在一直重启复制,起因是因为 NAT 网络中域名反解析出错导致高可用软件对复制的误判。 这里就能看出两个问题,第一是有个下发全局读锁的对象,个别在从库上就是备份工具了,第二就是 slave 正在被 kill,而且工夫相当长,因而这里可能存在一种非凡的死锁。 查看 mysql 过程时的偶合下,发现 mysqldump 过程已存在 10 多个小时,比照等 FTWRL 的过程的工夫,就坐实了下发全局读锁的对象是 mysqldump: 线上没有开启 performance_schema 的 instruments 和 consumers(PS:这个对于锁监控很重要,肯定记得关上)。如果开启了 performance_schema,能够通过 metadata_locks 查到相干锁记录,这个咱们在前面的复现中看一下。 上述情况剖析得出存在一个非凡的死锁,造成 MySQL Server 层和存储引擎层的死锁闭环,而且不能齐全追踪到所有锁记录。 解决:这样三个锁组合成的死锁在其余客户端执行 UNLOCKS TABLE 是解不开的,只须要 kill 掉全局读锁或者期待全局锁的锁一个即可,因为没有找到全局锁对应的线程,这里将等全局锁的线程 kill 掉,数据库就复原了,再看残留的 mysqldump 过程隐没了: 两个锁就此解开了 故障复原,提早追平。 ...

January 11, 2021 · 2 min · jiezi

MySQL-你好死锁

MySQL 你好,死锁 前言在日常的生活中,相信大家曾或多或少有这么一种体验:"每到下班高峰期的时候,原本宽坦的交通干道,一时间变得水泄不通,司机和乘客都烦躁不安,喇叭声响成一片,当车卡在十字路口中间,会很尴尬的发现,此时无论想走哪都…..."。对于这样的体验,大家都是十分的害怕接触和体验,交通部门也无时无刻为解决交通拥堵问题而努力。 其实上面生活案例中拥堵就类似于——高并发场景; 而所有方向的车堵在十字路口中间就类似于——数据库死锁场景。 本章主要围绕InnoDB存储引擎死锁相关的一些概念、产生死锁的原因、死锁场景以及死锁的处理策略。 相关概念为了更好的认识死锁,我们先来了解MySQL中与死锁相关的一些基本概念。 并发控制并发控制(Concurrency control)指的是当多个用户同时更新运行时,用于保护数据库完整性的各种技术。 读写锁为了保证数据库的并发控制,因此MySQL设置了两种锁: 共享锁(Shared Lock):也叫读锁(Read Lock),允许多个连接可以同一时刻并发的读取同一资源,互不干扰排他锁(Exclusive Lock):也叫写锁(Write Lock),会阻塞其他写锁或者读书的请求,保证同一时刻只有一个连接可以操作数据,包括读锁策略所谓锁策略就是在锁的开销和数据的安全性之间寻求平衡,这种平衡会影响到性能。目前InnoDB存储引擎有以下两种锁策略: Table Lock(表锁)策略:最基本的锁策略,开销最小,加锁快,不会出现死锁,但发生锁冲突概率高,粒度大,并发低Row Lock(行锁)策略:粒度最小,发生锁冲突态度低,并发也高,但是开销大,加锁慢,会出现死锁事务所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。一个事务是需要通过严格ACID测试的: 原子性(ATOMICITY):一个事务的整个操作,要么全部提交成功,要么全部失败回滚,不能执行其中的某一部分一致性(CONSISTENCY):数据库总是从一个一致性的状态转换到另外一个一致性的状态隔离性(ISOLATION):一个事物所作的修改在提交前,其他事务是看不到的持久性(DURABILITY):一旦事务提交,则其所做的修改就会永久保存到数据库中隔离级别SQL标准制定了四种隔离级别,规定事务的修改对其它事务是否可见 READ UNCOMMITED(未提交读):未提交也可见,又称脏读READ COMMITED (提交读):只有提交才可见,大多数DBMS默认隔离级别都是这个,MySQL不是,也称不可重复读REPEATABLE READ (可重复读),多次重复读取结果一致,MySQL默认这个级别,解决脏读问题,但存在幻读问题(某个事务读取记录时,另一事务插入了新纪录,原事务再读取记录时产生幻行)。SERIALIZABLE (可串行化),最高隔离级别,强制事务串行执行,避免了前面说的幻读问题,并发性能差隔离级别脏读可能性不可重复读可能性幻读可能性加锁读READ UNCOMMITEDYesYesYesNoREAD COMMITEDNoYesYesNoREPEATABLE READNoNoYesNoSERIALIZABLENoNoNoYes死锁的定义死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源(我等待你的资源,你却等待我的资源,我们都相互等待,谁也不释放自己占有的资源),从而导致恶性循环的现象: 当多个事务试图以不同顺序锁定资源时,就可能会产生死锁多个事务,同时锁定同一个资源时,也会产生死锁死锁的危害死等和死锁可不是一回事,如果你遇到了死等,大可放心,肯定不是死锁;如果发生了死锁,也大可放心,绝对不会死等。 这是因为MySQL内部有一套死锁检测机制,一旦发生死锁会立即回滚一个事务,让另一个事务执行下去。并且这个死锁回滚的的错误消息也会发送给客户端。即使正常的业务中,死锁也时不时会发生,所以遇到死锁不要害怕,因为这也是对数据安全的一种保护,但是若死锁太频繁,那可能会带来许多的问题: 使进程得不到正确的结果:处于死锁状态的进程得不到所需的资源,不能向前推进,故得不到结果使资源的利用率降低:处于死锁状态的进程不释放已占有的资源,以至于这些资源不能被其他进程利用,故系统资源利用率降低导致产生新的死锁:其它进程因请求不到死锁进程已占用的资源而无法向前推进,所以也会发生死锁死锁产生的原因死锁有四个必要的条件: 互斥排他:一个资源每次只能被一个进程使用保持着排他资源又提出新资源请求:一个进程因请求资源而阻塞时,对已获得的资源保持不放不可剥夺:资源不能被抢占,即资源只能在进程完成任务后自动释放环路:有一组等待进程{P0、P1、P2},P0等待的资源被P1所占有,P1等待的资源被P2所占有,而P2等待的又被P0所占有,形成了一个等待循环死锁的发生场景以下的所有场景是基于 InnoDB存储引擎并且隔离级别为REPEATABLE-READ(可重复读) 查询当前的隔离级别: select @@global.tx_isolation,@@tx_isolation;+-----------------------+-----------------+| @@global.tx_isolation | @@tx_isolation |+-----------------------+-----------------+| REPEATABLE-READ | REPEATABLE-READ |+-----------------------+-----------------+修改隔离级别: set global transaction isolation level read committed; ## 全局的set session transaction isolation level read committed; ## 当前会话(session)创建数据表 CREATE TABLE `deadlock` ( `id` int(11) NOT NULL, `stu_num` int(11) DEFAULT NULL, `score` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_uniq_stu_num` (`stu_num`), KEY `idx_score` (`score`)) ENGINE=InnoDB;insert into deadlock(id, stu_num, score) values (1, 11, 111);insert into deadlock(id, stu_num, score) values (2, 22, 222);insert into deadlock(id, stu_num, score) values (3, 33, 333);id主键索引 ...

August 19, 2019 · 3 min · jiezi

全栈之路JAVA基础课程三20190614v10

欢迎进入JAVA基础课程 本系列文章将主要针对JAVA一些基础知识点进行讲解,为平时归纳所总结,不管是刚接触JAVA开发菜鸟还是业界资深人士,都希望对广大同行带来一些帮助。若有问题请及时留言或加QQ:243042162。 谨记:最近习大大大力倡导“不忘初心、牢记使命”的主题教育,提出了“守初心、担使命、找差距、抓落实”的总体要求,这正好也映射在了我们个人生活和工作中。人到中年,最多的是懒,缺乏了学生时代的初心,不管你处于哪个年纪,请摆脱借口,端正态度,以家庭为寄托,以未来为展望,将技术提升和家庭教育落到实处,让自己有生之年还能得到质的飞跃。并发和多线程1. 进程和线程 进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。 线程:进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 区别:进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。 a.创建线程的几种方式(1)继承 Thread(2)实现 Runnable 接口(3)应用程序可以使用 Executor 框架来创建线程池b.线程的几种状态(早上打车去上班)(1)新建(准备叫一辆嘀嘀打车)(2)可运行(找到一辆可以带你去上班的车)(3)运行(司机接到你,带你去上班)(4)阻塞(路上堵车了):等待阻塞-wait、同步阻塞-同步锁、其他阻塞- Thread.sleep(long ms)或 t.join ()方法(5)死亡(到公司了,付钱下车)c.同步方法和同步代码块同步方法默认用 this 或者当前类 class 对象作为锁;同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法。死锁概念:两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。举例:某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。产生死锁原因:1. 系统资源的竞争2. 进程推进顺序非法3. 死锁产生的必要条件产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。(1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。(2)不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。(3)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。(4)循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。 代码块/** * 死锁实例 * t1先运行,这个时候flag==true,先锁定obj1,然后睡眠1秒钟 * 而t1在睡眠的时候,另一个线程t2启动,flag==false,先锁定obj2,然后也睡眠1秒钟 * t1睡眠结束后需要锁定obj2才能继续执行,而此时obj2已被t2锁定 * t2睡眠结束后需要锁定obj1才能继续执行,而此时obj1已被t1锁定 * t1、t2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */ class DeadLock implements Runnable{ private static Object obj1 = new Object(); private static Object obj2 = new Object(); private boolean flag; public DeadLock(boolean flag){ this.flag = flag; } @Override public void run(){ System.out.println(Thread.currentThread().getName() + "运行"); if(flag){ synchronized(obj1){ System.out.println(Thread.currentThread().getName() + "已经锁住obj1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj2){ // 执行不到这里 System.out.println("1秒钟后,"+Thread.currentThread().getName() + "锁住obj2"); } } }else{ synchronized(obj2){ System.out.println(Thread.currentThread().getName() + "已经锁住obj2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj1){ // 执行不到这里 System.out.println("1秒钟后,"+Thread.currentThread().getName() + "锁住obj1"); } } } }}public class LockDemo { public static void main(String[] args) { Thread t1 = new Thread(new DeadLock(true), "线程1"); Thread t2 = new Thread(new DeadLock(false), "线程2"); t1.start(); t2.start(); }}运行结果 ...

June 14, 2019 · 1 min · jiezi

Java死锁判断

首先我们使用自旋锁在来产生一个死锁现象代码:

May 31, 2019 · 1 min · jiezi

解决死锁的100种方法

死锁是多线程编程或者说是并发编程中的一个经典问题,也是我们在实际工作中很可能会碰到的问题。相信大部分读者对“死锁”这个词都是略有耳闻的,但从我对后端开发岗位的面试情况来看很多同学往往对死锁都还没有系统的了解。虽然“死锁”听起来很高深,但是实际上已经被研究得比较透彻,大部分的解决方法都非常成熟和清晰,所以大家完全不用担心这篇文章的难度。 虽然本文是一篇介绍死锁及其解决方式的文章,但是对于多线程程序中的非死锁问题我们也应该有所了解,这样才能写出正确且高效的多线程程序。多线程程序中的非死锁问题主要分为两类: 违反原子性问题 一些语句在底层会被分为多个底层指令运行,所以在多个线程之间这些指令就可能会存在穿插,这样程序的行为就可能会与预期不符造成bug。违反执行顺序问题 一些程序语句可能会因为子线程立即启动早于父线程中的后续代码,或者是多个线程并发执行等情况,造成程序运行顺序和期望不符导致产生bug。这两大非死锁多线程问题及其解决方案在之前的文章《多线程中那些看不到的陷阱》里都有详细的介绍,感兴趣的读者可以了解一下。 接下来就让我们开始消灭死锁吧! 初识死锁什么是死锁?死锁,顾名思义就是导致线程卡死的锁冲突,例如下面的这种情况: 线程t1线程t2获取锁A 获取锁B获取锁B(等待线程t2释放锁B) 获取锁A(等待线程t1释放锁A)可以看出,上面的两个线程已经互相卡死了,线程t1在等待线程t2释放锁B,而线程t2在等待线程t1释放锁A。两个线程互不相让也就没有一个线程可以继续往下执行了。这种情况下就发生了死锁。 死锁的四个必要条件上面的情况只是死锁的一个例子,我们可以用更精确的方式描述死锁出现的条件: 互斥。资源被竞争性地访问,这里的资源可以理解为锁;持有并等待。线程持有已经分配给他们的资源,同时等待其他的资源;不抢占。线程已经获取到的资源不会被其他线程强制抢占;环路等待。线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。上面这四个都是死锁出现的必要条件,如果其中任何一个条件不满足都不会出现死锁。虽然这四个条件的定义看起来非常的理论和官方,但是在实际的编程实践中,我们正是在死锁的这四个必要条件基础上构建出解决方案的。所以这里不妨思考一下这四个条件各自的含义,想一想如果去掉其中的一个条件死锁是否还能发生,或者为什么不能发生。 阻止死锁的发生了解了死锁的概念和四个必要条件之后,我们下面就正式开始解决死锁问题了。对于死锁问题,我们最希望能够达到的当然是完全不发生死锁问题,也就是在死锁发生之前就阻止它。 那么想要阻止死锁的发生,我们自然是要让死锁无法成立,最直接的方法当然是破坏掉死锁出现的必要条件。只要有任何一个必要条件无法成立,那么死锁也就没办法发生了。 破坏环路等待条件实践中最有效也是最常用的一种死锁阻止技术就是锁排序,通过对加锁的操作进行排序我们就能够破坏环路等待条件。例如当我们需要获取数组中某一个位置对应的锁来修改这个位置上保存的值时,如果需要同时获取多个位置对应的锁,那么我们就可以按位置在数组中的排列先后顺序统一从前往后加锁。 试想一下如果程序中所有需要加锁的代码都按照一个统一的固定顺序加锁,那么我们就可以想象锁被放在了一条不断向前延伸的直线上,而因为加锁的顺序一定是沿着这条线向下走的,所以每条线程都只能向前加锁,而不能再回头获取已经在后面的锁了。这样一来,线程只会向前单向等待锁释放,自然也就无法形成一个环路了。 其实大部分死锁解决方法不止可以用于多线程编程领域,还可以扩展到更多的并发场景下。比如在数据库操作中,如果我们要对某几行数据执行更新操作,那么就会获取这几行数据所对应的锁,我们同样可以通过对数据库更新语句进行排序来阻止在数据库层面发生的死锁。 但是这种方案也存在它的缺点,比如在大型系统当中,不同模块直接解耦和隔离得非常彻底,不同模块的研发同学之间都不清楚具体的实现细节,在这样的情况下就很难做到整个系统层面的全局锁排序了。在这种情况下,我们可以对方案进行扩充,例如Linux在内存映射代码就使用了一种锁分组排序的方式来解决这个问题。锁分组排序首先按模块将锁分为了不同的组,每个组之间定义了严格的加锁顺序,然后再在组内对具体的锁按规则进行排序,这样就保证了全局的加锁顺序一致。在Linux的对应的源码顶部,我们可以看到有非常详尽的注释定义了明确的锁排序规则。 这种解决方案如果规模过大的话即使可以实现也会非常的脆弱,只要有一个加锁操作没有遵守锁排序规则就有可能会引发死锁。不过在像微服务之类解耦比较充分的场景下,只要架构拆分合理,任务模块尽可能小且不会将加锁范围扩大到模块之外,那么锁排序将是一种非常实用和便捷的死锁阻止技术。 破坏持有并等待条件想要破坏持有并等待条件,我们可以一次性原子性地获取所有需要的锁,比如通过一个专门的全局锁作为加锁令牌控制加锁操作,只有获取了这个锁才能对其他锁执行加锁操作。这样对于一个线程来说就相当于一次性获取到了所有需要的锁,且除非等待加锁令牌否则在获取其他锁的过程中不会发生锁等待。 这样的解决方案虽然简单粗暴,但这种简单粗暴也带来了一些问题: 这种实现会降低系统的并发性,因为所有需要获取锁的线程都要去竞争同一个加锁令牌锁;并且因为要在程序的一开始就获取所有需要的锁,这就导致了线程持有锁的时间超出了实际需要,很多锁资源被长时间的持有所浪费,而其他线程只能等待之前的线程执行结束后统一释放所有锁;另一方面,现代程序设计理念要求我们提高程序的封装性,不同模块之间的细节要互相隐藏,这就使得在一个统一的位置一次性获取所有锁变得不再可能。破坏不抢占条件如果一个线程已经获取到了一些锁,那么在这个线程释放锁之前这些锁是不会被强制抢占的。但是为了防止死锁的发生,我们可以选择让线程在获取后续的锁失败时主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行了。 同样的,这个方案也会有自己的缺陷: 虽然这种方式可以避免死锁,但是如果几个互相存在竞争的线程不断地放弃、重试、放弃,那么就会导致活锁问题(livelock)。在这种情况下,虽然线程没有因为锁冲突被卡死,但是仍然会被阻塞相当长的时间甚至一直处于重试当中。 这个问题的一种解决方式是给任务重试添加一个随机的延迟时间,这样就能大大降低任务冲突的概率了。在一些接口请求框架中也使用了这种技巧来分散服务高峰期的请求重试操作,防止服务陷入阻塞、崩溃、阻塞的恶性循环。还是因为程序的封装性,在一个模块中难以释放其他模块中已经获取到的锁。虽然每一个方案都有自己的缺陷,但是在适合它们的场景下,它们都能发挥出巨大的作用。 破坏互斥条件在之前的文章中,我们已经了解了一种与锁完全不同的同步方式CAS。通过CAS提供的原子性支持,我们可以实现各种无锁数据结构,不仅避免了互斥锁所带来的开销和复杂性,也由此避开了我们一直在讨论的死锁问题。 AtomicInteger类中就大量使用了CAS操作来实现并发安全,例如incrementAndGet()方法就是用Unsafe类中基于CAS的原子累加方法getAndAddInt来实现的。下面是Unsafe类的getAndAddInt方法实现: /** * 增加指定字段值并返回原值 * * @param obj 目标对象 * @param valueOffset 目标字段的内存偏移量 * @param increment 增加值 * @return 字段原值 */public final int getAndAddInt(Object obj, long valueOffset, int increment) { // 保存字段原值的变量 int oldValue; do { // 获取字段原值 oldValue = this.getIntVolatile(obj, valueOffset); // obj和valueOffset唯一指定了目标字段所对应的内存区域 // while条件中不断调用CAS方法来对目标字段值进行增加,并保证字段的值没有被其他线程修改 // 如果在修改过程中其他线程修改了这个字段的值,那么CAS操作失败,循环语句会重试操作 } while(!this.compareAndSwapInt(obj, valueOffset, oldValue, oldValue + increment)); // 返回字段的原值 return oldValue;}上面代码中的compareAndSwapInt方法就是我们说的CAS操作(Compare And Swap),我们可以看到,CAS在每次执行时不一定会成功。如果执行CAS操作时目标字段的值已经被别的线程修改了,那么这次CAS操作就会失败,循环语句将会在CAS操作失败的情况下不断重试同样的操作。这种不断重试的方式就被称为自旋,在jvm当中对互斥锁的等待也会通过少量的自旋操作来进行优化。 ...

April 21, 2019 · 1 min · jiezi

[Java并发-4]关于Java的死锁

[Java并发-4]关于Java的死锁

April 10, 2019 · 1 min · jiezi

了解MySQL死锁日志

锁的种类&概念Shared and Exclusive LocksShared lock: 共享锁,官方描述:permits the transaction that holds the lock to read a roweg:select * from xx where a=1 lock in share modeExclusive Locks:排他锁: permits the transaction that holds the lock to update or delete a roweg: select * from xx where a=1 for updateIntention Locks这个锁是加在table上的,表示要对下一个层级(记录)进行加锁Intention shared (IS):Transaction T intends to set S locks on individual rows in table tIntention exclusive (IX): Transaction T intends to set X locks on those rows在数据库层看到的结果是这样的:TABLE LOCK table lc_3.a trx id 133588125 lock mode IXRecord Locks在数据库层看到的结果是这样的:RECORD LOCKS space id 281 page no 3 n bits 72 index PRIMARY of table lc_3.a trx id 133588125 lock_mode X locks rec but not gap该锁是加在索引上的(从上面的index PRIMARY of table lc_3.a 就能看出来)记录锁可以有两种类型:lock_mode X locks rec but not gap && lock_mode S locks rec but not gapGap Locks在数据库层看到的结果是这样的:RECORD LOCKS space id 281 page no 5 n bits 72 index idx_c of table lc_3.a trx id 133588125 lock_mode X locks gap before rec Gap锁是用来防止insert的Gap锁,中文名间隙锁,锁住的不是记录,而是范围,比如:(negative infinity, 10),(10, 11)区间,这里都是开区间哦Next-Key Locks在数据库层看到的结果是这样的:RECORD LOCKS space id 281 page no 5 n bits 72 index idx_c of table lc_3.a trx id 133588125 lock_mode XNext-Key Locks = Gap Locks + Record Locks 的结合, 不仅仅锁住记录,还会锁住间隙, 比如: (negative infinity, 10】,(10, 11】区间,这些右边都是闭区间哦Insert Intention Locks在数据库层看到的结果是这样的:RECORD LOCKS space id 279 page no 3 n bits 72 index PRIMARY of table lc_3.t1 trx id 133587907 lock_mode X insert intention waitingInsert Intention Locks 可以理解为特殊的Gap锁的一种,用以提升并发写入的性能AUTO-INC Locks在数据库层看到的结果是这样的:TABLE LOCK table xx trx id 7498948 lock mode AUTO-INC waiting属于表级别的锁自增锁的详细情况可以之前的一篇文章:http://keithlan.github.io/2017/03/03/auto_increment_lock/显示锁 vs 隐示锁显示锁(explicit lock)显示的加锁,在show engine innoDB status 中能够看到 ,会在内存中产生对象,占用内存 eg: select … for update , select … lock in share mode 隐示锁(implicit lock)implicit lock 是在索引中对记录逻辑的加锁,但是实际上不产生锁对象,不占用内存空间 哪些语句会产生implicit lock 呢? eg: insert into xx values(xx) eg: update xx set t=t+1 where id = 1 ; 会对辅助索引加implicit lockimplicit lock 在什么情况下会转换成 explicit lock eg: 只有implicit lock 产生冲突的时候,会自动转换成explicit lock,这样做的好处就是降低锁的开销 eg: 比如:我插入了一条记录10,本身这个记录加上implicit lock,如果这时候有人再去更新这条10的记录,那么就会自动转换成explicit lock数据库怎么知道implicit lock的存在呢?如何实现锁的转化呢?对于聚集索引上面的记录,有db_trx_id,如果该事务id在活跃事务列表中,那么说明还没有提交,那么implicit则存在对于非聚集索引:由于上面没有事务id,那么可以通过上面的主键id,再通过主键id上面的事务id来判断,不过算法要非常复杂,这里不做介绍记录锁,间隙锁,Next-key 锁和插入意向锁。这四种锁对应的死锁如下:记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap间隙锁(LOCK_GAP): lock_mode X locks gap before recNext-key 锁(LOCK_ORNIDARY): lock_mode X插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention表格信息: CREATE TABLE t_bitfly ( id bigint(20) NOT NULL DEFAULT ‘0’, num int(20) DEFAULT NULL, PRIMARY KEY (id), KEY num_key (num) ) ENGINE=InnoDB DEFAULT CHARSET=gbk; 表中数据: mysql> select * from t_bitfly; +—-+——+ | id | num | +—-+——+ | 1 | 2 | | 3 | 5 | | 8 | 7 | +—-+——+ 3 rows in set (0.04 sec) 数据库隔离级别为:可重复读(REPEATABLE-READ)模拟死锁场景:结果:insert into t_bitfly values(5,5)> 1213 - Deadlock found when trying to get lock; try restarting transaction> 时间: 0.085s查询日志 :show engine innodb status ;结果如下=====================================2018-08-05 21:20:27 0x7fd40c082700 INNODB MONITOR OUTPUT=====================================Per second averages calculated from the last 4 seconds—————–BACKGROUND THREAD—————–srv_master_thread loops: 251 srv_active, 0 srv_shutdown, 22663 srv_idlesrv_master_thread log flush and writes: 22905———-SEMAPHORES———-OS WAIT ARRAY INFO: reservation count 513OS WAIT ARRAY INFO: signal count 450RW-shared spins 0, rounds 569, OS waits 286RW-excl spins 0, rounds 127, OS waits 1RW-sx spins 0, rounds 0, OS waits 0Spin rounds per wait: 569.00 RW-shared, 127.00 RW-excl, 0.00 RW-sx————————LATEST DETECTED DEADLOCK————————2018-08-05 21:15:42 0x7fd40c0b3700*** (1) TRANSACTION:TRANSACTION 1095010, ACTIVE 21 sec insertingmysql tables in use 1, locked 1LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2MySQL thread id 16, OS thread handle 140548578129664, query id 3052 183.6.50.229 root updateinsert into t_bitfly values(7,7)*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 2514 page no 4 n bits 72 index num_key of table test.t_bitfly trx id 1095010 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32 0: len 4; hex 80000007; asc ;; 1: len 8; hex 8000000000000008; asc ;;*** (2) TRANSACTION:TRANSACTION 1095015, ACTIVE 6 sec insertingmysql tables in use 1, locked 14 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2MySQL thread id 17, OS thread handle 140548711855872, query id 3056 183.6.50.229 root updateinsert into t_bitfly values(5,5)*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 2514 page no 4 n bits 72 index num_key of table test.t_bitfly trx id 1095015 lock_mode XRecord lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0 0: len 8; hex 73757072656d756d; asc supremum;;Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32 0: len 4; hex 80000007; asc ;; 1: len 8; hex 8000000000000008; asc ;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 2514 page no 4 n bits 72 index num_key of table test.t_bitfly trx id 1095015 lock_mode X locks gap before rec insert intention waitingRecord lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32 0: len 4; hex 80000007; asc ;; 1: len 8; hex 8000000000000008; asc ;;省略。。。一些注释:LATEST DETECTED DEADLOCK:标示为最新发生的死锁;(1) TRANSACTION:此处表示事务1开始 ;MySQL thread id 16, OS thread handle 140548578129664, query id 3052 183.6.50.229 root update:此处为记录当前数据库线程id;insert into t_bitfly values(7,7):表示事务1在执行的sql ,不过比较悲伤的事情是show engine innodb status 是查看不到完整的事务的sql 的,通常显示当前正在等待锁的sql;(1) WAITING FOR THIS LOCK TO BE GRANTED:此处表示当前事务1等待获取行锁;(2) TRANSACTION:此处表示事务2开始 ;insert into t_bitfly values(5,5):表示事务2在执行的sql(2) HOLDS THE LOCK(S):此处表示当前事务2持有的行锁;(2) WAITING FOR THIS LOCK TO BE GRANTED:此处表示当前事务2等待获取行锁;根据死锁日志可以看出:事务一在执行insert into t_bitfly values(7,7)时,插入意向锁加锁时卡住;事务二在执行insert into t_bitfly values(5,5)时,持有next-key锁,插入意向锁加锁时卡住。结合上面执行的sql来分析:事务一执行delete from t_bitfly where num = 5 ;后,获取了 Gap Locks + Record Locks 也就是 next-key锁;事务二执行delete from t_bitfly where num = 7 ;后,获取了 Gap Locks + Record Locks 也就是 next-key锁;事务一执行insert into t_bitfly values(7,7)时,持有next-key锁,插入意向锁,等待事务二的next-key锁解锁;事务二执行insert into t_bitfly values(5,5)时,持有next-key锁,插入意向锁,等待事务二的next-key锁解锁;产生死锁。 ...

April 1, 2019 · 4 min · jiezi

一个游戏拨账系统的数据库结算设计

假设现存在一个简单的猜大小游戏,由用户下注大或者小,扣除手续费3%后的钱全部放入奖池中,赢的一方按投注比例平分整个奖池。使用mysql作为数据库,系统精度精确到1位小数。 本文将会讲解其中会出现的业务结算导致的数据问题,以及解决方法。数据库逻辑设计系统内应该存在一个用户钱包表,其中指定两条记录为系统收入账户和系统拨出账户。这样可以将投注的时候,对系统账户余额增加操作,和发奖的时候,对系统账户余额的减去操作分离。可以避免上一期游戏的结算,对下一期游戏的投注发生锁等待的问题。业务加锁考虑到高并发的情况下,推荐使用mysql自带的排他锁,不推荐乐观锁,因为乐观锁需要重试机制,而队列结算暂时不考虑。 当一名用户发起投注的时候,检查顺序应该如下检查系统游戏开关(冗余) 查询一次用户余额是否大于这次下注金额开启事务对系统收入账户加排他锁对用户收入账户加排他锁检查用户余额是否足够对用户进行扣款对系统进行收款为奖池加入97%的投注额度事务提交这里之所以要冗余检查用户的额度,是否了避免开启事务的消耗,防止恶意攻击消耗系统资源,用来开启无意义事务。奖池额度的97%这里计算需要保持一位精度,如果用户投注是98,按照计算得到的值应该是95.06,我们应该取95.0而不是95.1,否则你最后存到奖池里面的数就会大于97%,这样系统抽取就不会达到3%,用户少分点没关系,要保证系统一定能分到3%。简单一句话就是:精度位后都舍弃发奖过程设计假设按照投注比例,瓜分出的奖金总数是22.1,A用户的份额是55.5%,A用户拿到12.2655,B用户的份额是45%,B用户拿到9.8345。这种情况下,你会发现,按照舍弃,原则,分别是12.2和9.8,结果是只发放了22,如果你按照四舍五入原则,才能发放到22.1那为什么还要坚持舍弃原则呢?因为,假设出一个极端情况,当你碰到A的值是12.05,B的值是9.05,按照舍弃原则,总数的确还是22.1。但是按照四舍五入原则,发放的总值就是22.2了。结语在计算机系统内,浮点数的计算本身就是不可靠的,在业务内应该用整形去避免,当设计到百分比操作的时候,请尽量使用舍弃原则,保证不多发。按照舍弃原则,给用户少发0.05这种精度外的值,对业务来说无关紧要。如果超发了,会导致系统内账目混乱,后果将不堪设想。

January 28, 2019 · 1 min · jiezi