数据库死锁是并发场景下的常见问题,当两个或多个事务相互持有对方需要的资源(锁),且都无法释放已持有的资源时,会形成无限等待的 “僵局”。死锁会导致事务阻塞、业务超时,严重时可能引发批量请求失败,甚至数据库雪崩。作为运维工程师,掌握死锁的快速排查、精准定位、有效解决及预防能力,是保障数据库稳定性的核心技能。
一、死锁的本质与产生条件
在解决死锁前,需明确其底层逻辑:死锁的产生必须同时满足四个条件(缺一不可):
互斥条件:资源(如行锁、表锁)只能被一个事务持有;
持有并等待:事务已持有至少一个资源,且在等待其他资源;
不可剥夺:事务已持有的资源不能被强制剥夺;
循环等待:多个事务形成闭环等待链(如 T1 等待 T2 的资源,T2 等待 T1 的资源)。
典型场景:电商订单系统中,两个并发事务同时处理 “订单表” 和 “库存表”:
事务 1:更新订单表(持有订单行锁)→ 尝试更新库存表(等待库存行锁);
事务 2:更新库存表(持有库存行锁)→ 尝试更新订单表(等待订单行锁);
此时形成循环等待,触发死锁。
二、死锁排查:从 “发现” 到 “定位根因”
死锁排查的核心是获取死锁日志→解析日志信息→还原事务执行逻辑。不同数据库的死锁日志获取方式差异较大,需针对性操作。
1. 第一步:发现死锁(监控与告警)
死锁发生时,数据库通常会自动终止其中一个事务(“牺牲者”)以打破僵局,同时在日志中记录死锁信息。需通过以下方式及时发现:
应用层告警:业务接口出现 “deadlock found when trying to get lock”(MySQL)、“ORA-00060: deadlock detected while waiting for resource”(Oracle)等错误;
数据库监控:通过工具监控锁等待指标(如 MySQL 的Innodb_deadlocks计数器、SQL Server 的Deadlock Graph、Oracle 的v$deadlock视图),设置阈值告警(如 1 分钟内死锁次数 > 3 次)。
2. 第二步:获取死锁日志(关键操作)
死锁日志包含涉事事务、持有 / 等待的锁类型、执行的 SQL 语句等关键信息,是定位根因的核心依据。
(1)MySQL(InnoDB 存储引擎)
InnoDB 会将死锁信息记录在innodb_status中,可通过以下方式查看:
sql
— 方式1:实时查看死锁日志(仅显示最近一次死锁)
SHOW ENGINE INNODB STATUS\G
— 方式2:配置日志持久化(推荐生产环境)
# 在my.cnf中开启,死锁日志会写入错误日志
innodb_print_all_deadlocks = 1
日志关键信息解析:
plaintext
LATEST DETECTED DEADLOCK
————————
2023-10-01 10:00:00 0x7f1234567890
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 140737354123456, query id 1000 localhost root updating
UPDATE order SET status=1 WHERE id=100 — 事务1执行的SQL
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5 page no 3 n bits 72 index PRIMARY of table `test`.`order` trx id 12345 lock_mode X locks rec but not gap waiting — 等待order表id=100的排他行锁
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 11, OS thread handle 140737354123457, query id 1001 localhost root updating
UPDATE inventory SET num=num-1 WHERE goods_id=200 — 事务2执行的SQL
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 5 page no 3 n bits 72 index PRIMARY of table `test`.`order` trx id 12346 lock_mode X locks rec but not gap — 持有order表id=100的排他行锁
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 6 page no 4 n bits 80 index PRIMARY of table `test`.`inventory` trx id 12346 lock_mode X locks rec but not gap waiting — 等待inventory表goods_id=200的排他行锁
*** WE ROLL BACK TRANSACTION (1) — 数据库回滚事务1以解除死锁
(2)Oracle
Oracle 死锁信息会记录在告警日志(alert.log) 和跟踪文件(trace file) 中:
步骤 1:通过v$deadlock视图定位死锁跟踪文件路径:
sql
SELECT p.spid, t.* FROM v$process p, v$session s, v$deadlock t
WHERE p.addr = s.paddr AND s.sid = t.sid;
步骤 2:在user_dump_dest目录下找到对应 trace 文件(如ora_1234.trc),解析死锁信息:
plaintext
DEADLOCK DETECTED (ORA-00060)
[Transaction Deadlock]
The following deadlock is not an ORACLE error. It is a deadlock due to user error in the design of an application or from issuing incorrect ad-hoc SQL.
…
Resource Name holder waiters
TX-00010002-00003456 123 456 — 事务123持有TX锁,事务456等待
TX-00030004-00007890 456 123 — 事务456持有TX锁,事务123等待
…
(3)SQL Server
SQL Server 通过死锁图(Deadlock Graph) 可视化展示死锁信息,获取方式:
方式 1:通过 SQL Server Profiler 跟踪 “Deadlock Graph” 事件,生成 XML 格式的死锁图;
方式 2:在system_health扩展事件会话中查询(默认记录死锁):
sql
SELECT
XEvent.query(‘(event/data/value/deadlock)[1]’) AS DeadlockGraph
FROM (
SELECT XEvent.query(‘.’) AS XEvent
FROM (
SELECT CAST(target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE s.name = ‘system_health’ AND st.target_name = ‘event_file’
) AS Data
CROSS APPLY TargetData.nodes(‘EventFileTarget/Event’) AS XEventData(XEvent)
WHERE XEventData.XEvent.value(‘@name’, ‘varchar(4000)’) = ‘xml_deadlock_report’
) AS Results;
死锁图中会清晰标注涉事进程(Process)、持有锁(Owner)、等待锁(Waiter)及执行的 SQL。
3. 第三步:分析死锁原因(核心逻辑)
结合死锁日志,重点分析以下维度:
锁类型:是行锁(Row Lock)、表锁(Table Lock)还是间隙锁(Gap Lock)?表锁更容易引发死锁(范围更大);
事务逻辑:涉事事务是否持有过多锁?是否存在长事务(持有锁时间过长)?
SQL 执行顺序:事务是否因 “更新表的顺序不一致” 导致循环等待(如 T1 先更 A 表再更 B 表,T2 先更 B 表再更 A 表);
索引情况:是否因缺少索引导致 “行锁升级为表锁”(如 MySQL 中无索引的UPDATE会锁全表)。
三、死锁解决:从 “临时修复” 到 “彻底根除”
解决死锁的核心是打破死锁产生的四个条件(尤其是 “循环等待” 和 “持有并等待”),具体方法需结合业务场景选择。
1. 紧急处理:快速解除当前死锁
当死锁导致业务阻塞时,需立即干预:
终止事务:通过数据库命令终止 “牺牲者” 事务(若数据库未自动处理):
MySQL:KILL [线程ID](死锁日志中的MySQL thread id);
Oracle:ALTER SYSTEM KILL SESSION ‘[sid, serial#]’(从v$session获取);
SQL Server:KILL [进程ID](死锁图中的spid)。
临时降级隔离级别:若死锁频繁发生在高隔离级别(如REPEATABLE READ),可临时降低为READ COMMITTED(减少锁持有时间),但需评估业务对一致性的容忍度。
2. 根本解决:针对性优化方案
根据死锁原因,采取以下长效措施:
(1)统一事务操作顺序(打破 “循环等待”)
最有效的方法是让所有事务按相同顺序操作资源。例如:
原逻辑:T1 先更 A 表再更 B 表,T2 先更 B 表再更 A 表→ 改为:T1 和 T2 均先更 A 表,再更 B 表;
实现方式:在应用层封装统一的更新接口(如 “先操作订单表,再操作库存表”),避免不同业务模块按不同顺序执行。
(2)减少锁持有时间(打破 “持有并等待”)
事务持有锁的时间越短,死锁概率越低:
拆分长事务:将一个大事务拆分为多个小事务(如 “下单→扣库存→日志记录” 拆分为 3 个独立事务);
优化 SQL 执行效率:避免在事务中执行慢查询(如全表扫描),通过加索引、改写 SQL 减少执行时间(如将UPDATE t SET … WHERE name LIKE ‘%xxx%’优化为带索引的精确查询);
避免事务中等待用户输入:禁止在事务中调用需要人工确认的操作(如 “下单后等待用户支付确认” 应将支付确认作为独立事务)。
(3)控制锁的粒度(减少 “互斥资源范围”)
锁的粒度越大(如表锁),冲突概率越高,需尽可能使用细粒度锁:
添加有效索引:避免因无索引导致行锁升级为表锁(如 MySQL 中UPDATE t SET … WHERE col若col无索引,会锁全表);
避免使用表级锁:禁用显式表锁语句(如LOCK TABLES),优先使用行锁;
慎用间隙锁:MySQL 在REPEATABLE READ隔离级别下,范围查询(如WHERE id > 100)会产生间隙锁,可通过降低隔离级别或使用FOR UPDATE SKIP LOCKED(MySQL 8.0+)跳过锁定行。
(4)使用乐观锁替代悲观锁(适用于低冲突场景)
在并发冲突较少的场景(如电商商品详情页更新),用乐观锁(基于版本号)替代悲观锁(行锁),从根本上避免锁竞争:
sql
— 乐观锁实现:更新时检查版本号,仅当版本一致时更新
UPDATE goods
SET stock = stock – 1, version = version + 1
WHERE id = 100 AND version = 5; — version为当前版本号
若更新失败(受影响行数 = 0),说明已被其他事务修改,应用层重试即可。
四、死锁预防:构建 “主动防御” 体系
死锁无法完全杜绝,但可通过以下措施降低发生概率:
规范事务设计:
强制 “事务仅操作必要资源”(不访问无关表);
设定事务超时时间(如 MySQL 的innodb_lock_wait_timeout=5秒,超时后自动放弃)。
定期审计锁相关指标:
监控锁等待次数(Innodb_lock_waits)、锁等待时间(Innodb_lock_wait_time_avg);
每周分析慢查询日志中的长事务(运行时间 > 10 秒),评估是否存在锁滥用。
压力测试验证:
在上线前通过压测工具(如 SysBench、JMeter)模拟高并发场景,故意设计 “无序更新” 等逻辑,验证是否会产生死锁,提前优化。
使用数据库原生机制:
MySQL 8.0 + 支持SELECT … FOR UPDATE SKIP LOCKED跳过已锁定行,避免等待;
Oracle 的FOR UPDATE NOWAIT若获取锁失败立即报错,不进入等待;
SQL Server 的SET DEADLOCK_PRIORITY LOW指定事务为低优先级,发生死锁时优先被回滚。
总结
死锁排查与解决的核心逻辑是 “日志驱动分析,逻辑优化破局”:通过死锁日志定位涉事事务与锁资源,从 “操作顺序、锁持有时间、锁粒度” 三个维度优化,最终通过规范设计和监控预防再生。对于运维工程师,需熟悉不同数据库的死锁日志获取方式,具备 “从日志到业务逻辑” 的逆向推导能力,同时与开发团队协作,将死锁优化嵌入业务开发流程(而非事后救火),才能从根本上保障数据库的并发稳定性。
原创文章,作者:DEV编辑,如若转载,请注明出处:https://devcn.xin/5632.html