虽然锁在一定程度上能够解决并发问题, 但稍有不慎, 就可能造成死锁. 本文介绍死锁的产生及处理.
1 死锁的产生和预防
发生死锁的必要条件有 4 个, 分别为互斥条件, 不可剥夺条件, 请求与保持条件和循环等待条件, 如图 1-6 所示.
▲图 1-6 死锁的必要条件
1. 互斥条件
在一段时间内, 计算机中的某个资源只能被一个进程占用. 此时, 如果其他进程请求该资源, 则只能等待.
2. 不可剥夺条件
某个进程获得的资源在使用完毕之前, 不能被其他进程强行夺走, 只能由获得资源的进程主动释放.
3. 请求与保持条件
进程已经获得了至少一个资源, 又要请求其他资源, 但请求的资源已经被其他进程占有, 此时请求的进程就会被阻塞, 并且不会释放自己已获得的资源.
4. 循环等待条件
系统中的进程之间相互等待, 同时各自占用的资源又会被下一个进程所请求. 例如有进程 A, 进程 B 和进程 C 三个进程, 进程 A 请求的资源被进程 B 占用, 进程 B 请求的资源被进程 C 占用, 进程 C 请求的资源被进程 A 占用, 于是形成了循环等待条件, 如图 1-7 所示.
▲图 1-7 死锁的循环等待条件
需要注意的是, 只有 4 个必要条件都满足时, 才会发生死锁.
处理死锁有 4 种方法, 分别为预防死锁, 避免死锁, 检测死锁和解除死锁, 如图 1-8 所示.
▲图 1-8 处理死锁的方法
预防死锁: 处理死锁最直接的方法就是破坏造成死锁的 4 个必要条件中的一个或多个, 以防止死锁的发生.
避免死锁: 在系统资源的分配过程中, 使用某种策略或者方法防止系统进入不安全状态, 从而避免死锁的发生.
检测死锁: 这种方法允许系统在运行过程中发生死锁, 但是能够检测死锁的发生, 并采取适当的措施清除死锁.
解除死锁: 当检测出死锁后, 采用适当的策略和方法将进程从死锁状态解脱出来.
在实际工作中, 通常采用有序资源分配法和银行家算法这两种方式来避免死锁, 大家可自行了解.
2 MySQL 中的死锁问题
在 MySQL 5.5.5 及以上版本中, MySQL 的默认存储引擎是 InnoDB. 该存储引擎使用的是行级锁, 在某种情况下会产生死锁问题, 所以 InnoDB 存储引擎采用了一种叫作等待图 (wait-for graph) 的方法来自动检测死锁, 如果发现死锁, 就会自动回滚一个事务.
接下来, 我们看一个 MySQL 中的死锁案例.
第一步: 打开终端 A, 登录 MySQL, 将事务隔离级别设置为可重复读, 开启事务后为 account 数据表中 id 为 1 的数据添加排他锁, 如下所示.
- MySQL> set session transaction isolation level repeatable read;
- Query OK, 0 rows affected (0.00 sec)
- MySQL> start transaction;
- Query OK, 0 rows affected (0.00 sec)
- MySQL> select * from account where id =1 for update;
- +----+--------+---------+
- | id | name | balance |
- +----+--------+---------+
| 1 | 张三 | 300 |
- +----+--------+---------+
- 1 row in set (0.00 sec)
第二步: 打开终端 B, 登录 MySQL, 将事务隔离级别设置为可重复读, 开启事务后为 account 数据表中 id 为 2 的数据添加排他锁, 如下所示.
- MySQL> set session transaction isolation level repeatable read;
- Query OK, 0 rows affected (0.00 sec)
- MySQL> start transaction;
- Query OK, 0 rows affected (0.00 sec)
- MySQL> select * from account where id =2 for update;
- +----+--------+---------+
- | id | name | balance |
- +----+--------+---------+
| 2 | 李四 | 350 |
- +----+--------+---------+
- 1 row in set (0.00 sec)
第三步: 在终端 A 为 account 数据表中 id 为 2 的数据添加排他锁, 如下所示.
MySQL> select * from account where id =2 for update;
此时, 线程会一直卡住, 因为在等待终端 B 中 id 为 2 的数据释放排他锁.
第四步: 在终端 B 中为 account 数据表中 id 为 1 的数据添加排他锁, 如下所示.
- MySQL> select * from account where id =1 for update;
- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
此时发生了死锁. 通过如下命令可以查看死锁的日志信息.
show engine innodb status\G
通过命令行查看 LATEST DETECTED DEADLOCK 选项相关的信息, 可以发现死锁的相关信息, 或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2 版本开始提供)参数为 ON, 将死锁相关信息打印到 MySQL 错误日志中.
在 MySQL 中, 通常通过以下几种方式来避免死锁.
尽量让数据表中的数据检索都通过索引来完成, 避免无效索引导致行锁升级为表锁.
合理设计索引, 尽量缩小锁的范围.
尽量减少查询条件的范围, 尽量避免间隙锁或缩小间隙锁的范围.
尽量控制事务的大小, 减少一次事务锁定的资源数量, 缩短锁定资源的时间.
如果一条 SQL 语句涉及事务加锁操作, 则尽量将其放在整个事务的最后执行.
尽可能使用低级别的事务隔离机制.
关于作者: 肖宇, 分布式事务架构专家, Apache ShenYu(incubating)网关创始人, Dromara 开源组织创始人, Hmily,RainCat,Myth 等分布式事务框架的作者. Apache ShardingSphere Committer.
冰河, 互联网高级技术专家, MySQL 技术专家, 分布式事务架构专家. 多年来, 一直致力于分布式系统架构, 微服务, 分布式数据库, 分布式事务与大数据技术的研究, 在高并发, 高可用, 高可扩展性, 高可维护性和大数据等领域拥有丰富的架构经验.
本文摘编自《深入理解分布式事务: 原理与实战》, 经出版方授权发布.
来源: http://database.51cto.com/art/202110/686592.htm