一、问题
数据库的死锁是怎么产生的?两个事务抢锁,谁也不让谁就产生死锁了。
出现死锁了怎么排查和解决?
二、死锁的原理
死锁的原理特别好理解,比如:你和朋友去吃饭,你拿了筷子,他拿了碗,你要等他的碗才能吃饭,他要等你筷子才能吃饭,你们两个谁也不松手,然后就僵在这里了,这个就是死锁。
对应到数据库里面,必须同时满足 4 个条件才会出现死锁:
互斥条件:一个锁同一时间只能被一个事务持有,就像筷子同一时间只能由一个人去用
持有并等待:我已经拿了一个锁,还要等着拿另一个锁,手里的锁呢我还不松手
不可剥夺:别人拿的锁我不能硬抢,只能等别人自己释放
循环等待:a 等 b 的锁,b 等 a 的锁,形成了一个死循环
这 4 个条件同时满足就形成了死锁。
三、死锁的定位
3.1、SQL
show engine innodb status
这个语句会清楚的告诉你哪两个事务产生了死锁,这个两个事务分别执行了什么 SQL,抢的是什么类型的锁,最后数据库自动回滚了哪一个事务
3.2、业务代码找问题
拿着死锁的日志里面的两个 SQL 语句,去找对应的业务代码。
90% 的情况都是事务执行顺序反了:比如说事务 1 先改订单表,再改用户表;事务 2 先改用户表,再改订单表;刚好形成一个循环等待
3.3、检查不必要锁
看是不是锁的粒度太大了:比如明明应该加行锁的,结果因为字段没有加索引,导致了锁所有的行。
普通查询加了一些没必要的悲观锁:比说 for update 导致锁的范围变大了,更容易产生冲突
四、死锁的解决
4.1、统一事务执行顺序
所有涉及到多表修改的事务, 都按同一个顺序来执行。
比如说所有改订单和用户的表的事务,都先改订单再改用户,大家按照同一个的顺序去拿锁,就不会出现循环等待。
4.2、大事务拆成小事务
锁的持有时间越短,冲突的概率也就越低。
不要在事务里面去,加 RPC 的调用、去查 Redis,不要去处理业务逻辑这些无关的操作,尽量的让事务只做数据库的操作。
并且SQL不要去处理复杂的业务逻辑,把业务逻辑下沉到代码里面去做,快点执行完提交释放锁
4.3、降低锁的粒度
能用行锁就不要去用表锁。
能用乐观锁就不要去用悲观锁,比如说你可以用版本号实现乐观锁,不用去用 for update 去加悲观锁。
锁的范围越小,冲突的概率也就越低。
你在改数据的时候,一定要基于索引去改。
4.4、设置锁超时时间兜底
MySQL 里面有一个 lock_wait_timeout 的参数,可以设置锁的等待时间。
超过时间它会自动回滚其中一个事务,让另外一个事务能正常地执行,不会一直僵住导致业务不可用。
五、死锁的发送概率
死锁其实很少发生:
大部分都是因为代码写的不规范。
事务执行的顺序乱了导致的,只要统一了执行顺序,基本上就能避免 99% 的死锁问题。
mysql 第8.17章 事务-死锁