SQL事务死锁指的是两个或多个事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象,若无外力作用这些事务将无法继续推进。死锁会导致部分事务回滚,影响业务数据的完整性和执行效率,是数据库运维和开发中需要重点关注的问题。
死锁产生的常见原因
死锁的出现通常和事务的执行逻辑、锁的获取顺序有关,常见诱因包括以下几种:
- 多个事务以不同的顺序获取相同资源的锁,比如事务A先锁表1再锁表2,事务B先锁表2再锁表1,就容易形成互相等待
- 事务执行时间过长,长时间持有锁资源不释放,增加了和其他事务冲突的概率
- 锁的范围过大,比如不必要的表级锁、长事务中的范围锁,会提升死锁发生的可能性
- 索引缺失导致查询走全表扫描,触发大量的行锁升级或者范围锁,进而引发死锁
死锁检测方法
MySQL数据库死锁检测
MySQL默认开启了死锁检测机制,当检测到死锁时会自动回滚其中一个事务来打破死锁。我们可以通过以下方式查看死锁相关信息:
查看最近一次死锁的详细日志,执行如下SQL语句:
SHOW ENGINE INNODB STATUS;
日志中DEADLOCK INFORMATION部分会记录死锁涉及的事务、SQL语句、持有的锁和等待的锁等详细信息,方便我们定位问题。
如果需要持续监控死锁,可以开启死锁日志输出,修改MySQL配置文件添加如下配置后重启服务:
[mysqld] innodb_print_all_deadlocks = 1
SQL Server数据库死锁检测
SQL Server可以通过系统视图和扩展事件来检测死锁,常用的查询方式如下:
查询当前实例中发生的死锁相关记录:
SELECT
xdl.value('(event/@name)[1]', 'varchar(50)') AS event_name,
xdl.value('(event/@timestamp)[1]', 'datetime2') AS event_time,
xdl.value('(event/data[@name="deadlock_cycle"]/value)[1]', 'xml') AS deadlock_xml
FROM sys.fn_xe_file_target_read_file('C:SqlDeadlockLogs*.xel', NULL, NULL, NULL) AS f
CROSS APPLY f.event_data.nodes('//event') AS xdl(xdl);
也可以通过SQL Server Profiler工具跟踪死锁事件,实时获取死锁的发生情况和详细信息。
死锁解决方案
优化事务逻辑
统一事务中锁资源的获取顺序,所有事务都按照相同的顺序申请锁,从根源上避免不同顺序加锁导致的死锁。同时尽量缩短事务的执行时间,把非必要的逻辑放到事务外部,减少锁的持有时长。
优化索引设计
为查询语句涉及的字段添加合适的索引,避免全表扫描导致的大范围锁。比如以下查询如果user_id字段没有索引,会锁定大量行,添加索引后可以只锁定符合条件的行:
-- 原查询 SELECT * FROM orders WHERE user_id = 1001 FOR UPDATE; -- 为user_id添加索引 CREATE INDEX idx_orders_user_id ON orders(user_id);
调整锁的粒度
尽量避免使用表级锁,优先使用行级锁。如果业务允许,可以适当降低事务的隔离级别,比如从可重复读调整为读已提交,减少间隙锁的使用,降低死锁发生的概率。
死锁后的重试机制
在应用层添加事务重试逻辑,当捕获到死锁导致的异常时,等待随机时间后重新执行事务,通常重试2-3次即可成功执行。以下是Java中的简单重试示例:
public void executeWithRetry() {
int retryCount = 0;
while (retryCount < 3) {
try {
// 执行数据库事务逻辑
doBusiness();
break;
} catch (DeadlockException e) {
retryCount++;
try {
// 随机等待100-500毫秒
Thread.sleep((long) (Math.random() * 400 + 100));
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
死锁预防建议
日常开发中可以通过以下方式预防死锁:事务中尽量一次获取所有需要的锁,避免逐步申请;给事务设置合理的超时时间,避免事务长时间阻塞;定期分析慢查询日志,优化耗时较长的SQL语句,减少长事务的出现。