外观
什么是死锁
死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象,导致这些事务都无法继续执行下去,程序陷入无限期的阻塞状态。
举个简单的例子,有两个事务 T1 和 T2,事务 T1 持有资源 A 的锁并请求资源 B 的锁,而事务 T2 持有资源 B 的锁并请求资源 A 的锁,此时 T1 和 T2 就会互相等待对方释放锁,从而形成死锁。
死锁产生的必要条件
- 互斥条件:资源在同一时间只能被一个事务使用,即排他性使用。例如,一个排他锁锁定的资源,在锁被释放前只能被持有该锁的事务访问。
- 请求和保持条件:事务已经持有了至少一个资源,并且在请求其他资源时,不释放已持有的资源。比如事务 T1 已经持有资源 A 的锁,在请求资源 B 的锁时,不会释放资源 A 的锁。
- 不剥夺条件:事务持有的资源不能被其他事务强行剥夺,只能由持有该资源的事务主动释放。
- 循环等待条件:多个事务之间形成一种首尾相连的循环等待资源的关系。就像前面例子中的 T1 等待 T2 释放资源 B,T2 等待 T1 释放资源 A。
死锁的解决办法
预防死锁
- 破坏互斥条件:一般来说,数据库中的资源需要互斥访问,所以很难破坏这个条件。不过在某些情况下,可以通过资源的共享使用来避免死锁,但这不适用于需要排他访问的场景。
- 破坏请求和保持条件:可以采用一次性分配资源的策略,即事务在开始执行前,一次性请求它所需要的所有资源。如果资源不能全部满足,则事务等待,直到所有资源都可用。在数据库中,这种方法可能不太现实,因为事务在执行过程中可能无法提前知道需要哪些资源。
- 破坏不剥夺条件:允许事务在某些情况下剥夺其他事务持有的资源。例如,当一个事务请求的资源被其他事务持有时,可以设置一定的规则,让持有资源的事务释放该资源。在数据库中,可以通过设置锁的超时时间来实现类似的效果。
- 破坏循环等待条件:对资源进行排序,事务按照一定的顺序请求资源。这样可以避免循环等待的情况发生。例如,在数据库中,可以规定事务必须按照表的字母顺序来请求锁。
检测死锁
- 超时机制:为每个事务的锁请求设置一个超时时间,如果在规定的时间内没有获得所需的锁,则认为可能发生了死锁,事务会回滚。例如,在 MySQL 中,可以通过设置
innodb_lock_wait_timeout
参数来控制锁等待的超时时间。 - 等待图法:数据库系统会维护一个等待图,图中的节点表示事务,边表示事务之间的等待关系。如果等待图中出现了环,则表示存在死锁。数据库会定期检查等待图,一旦发现死锁,就会选择一个事务作为牺牲品进行回滚,以解除死锁。
解除死锁
- 选择牺牲品:数据库系统会根据一定的规则选择一个或多个事务作为牺牲品进行回滚。通常会选择回滚代价最小的事务,例如执行时间最短、修改数据量最少的事务。
- 回滚事务:将选择的牺牲品事务回滚到事务开始前的状态,释放该事务持有的所有锁,使其他事务可以继续执行。
代码示例(以 MySQL 为例)
-- 设置锁等待超时时间
SET GLOBAL innodb_lock_wait_timeout = 5;
-- 模拟死锁场景
-- 会话 1
START TRANSACTION;
UPDATE table1 SET column1 = 'value1' WHERE id = 1;
-- 此时暂停一段时间,等待会话 2 执行部分操作
SELECT SLEEP(2);
UPDATE table2 SET column2 = 'value2' WHERE id = 1;
COMMIT;
-- 会话 2
START TRANSACTION;
UPDATE table2 SET column2 = 'new_value2' WHERE id = 1;
-- 此时暂停一段时间,等待会话 1 执行部分操作
SELECT SLEEP(2);
UPDATE table1 SET column1 = 'new_value1' WHERE id = 1;
COMMIT;
在上述示例中,如果发生死锁,由于设置了 innodb_lock_wait_timeout
为 5 秒,当某个事务等待锁的时间超过 5 秒时,会自动回滚,从而解除死锁。