分布式数据库的死锁,本质上和单机数据库逻辑相通,但排查难度呈指数级上升。单机死锁通常发生在同一个实例内的锁竞争,而分布式死锁则横跨多个网络节点,涉及全局事务协调器、参与者状态不一致、网络分区幽灵等复杂场景。真正棘手的是,某些攻击者已经学会利用分布式事务的等待超时机制和重试策略,通过刻意制造死锁来拖垮整个数据库集群的吞吐能力。
在分布式架构下,死锁的形态已经从传统的“循环等待”演变为更隐蔽的“等待图跨节点扩散”。比如一个全局事务在节点A持有行锁,同时等待节点B释放资源;而节点B上的另一个事务正好持有该资源,又在等待节点C;节点C的事务链最终又绕回节点A。这种跨节点的循环等待,往往因为各节点本地死锁检测器无法看到全局视图而被忽略,直到集群出现大面积超时告警,业务侧已经明显感知到卡顿。
全局等待图构建与死锁检测算法解决分布式死锁的核心在于构建全局等待图。每个节点需要将本地事务的锁等待关系实时上报给一个中心化或分布式的死锁检测模块。常见的实现方式有两种:一种是集中式检测,所有节点定期向协调器发送“事务ID-等待事务ID-持有资源”的三元组信息,协调器在内存中拼接成完整的有向图,运行周期性的环检测算法;另一种是分布式路径追踪,将等待关系沿着事务链逐跳传递,如果某个事务收到了包含自己ID的等待链,说明形成了环。
集中式方案实现简单,但协调器容易成为性能瓶颈和单点故障点。工程上通常会采用多副本协调器加租约机制,保证检测服务的高可用。分布式路径追踪则更去中心化,但对网络消息的可靠性要求极高,一旦某条路径消息丢失,可能导致死锁长时间无法被发现。实际落地时,TiDB 采用的就是在 TiKV 层做悲观锁冲突检测,结合 TiDB Server 层的全局事务管理器来统一裁决死锁;而 CockroachDB 则依赖其分布式死锁检测算法,通过心跳消息携带等待信息,在节点间传播构建等待图。
死锁受害者选择与事务回滚策略检测到死锁环之后,系统必须挑选一个事务作为“受害者”进行回滚,以打破循环。选择策略直接影响系统公平性和业务体验。最简单的策略是回滚最年轻的事务,即启动时间最晚的那个,这种策略实现成本低,但可能导致某些长事务永远无法完成,陷入“饥饿”状态。更合理的做法是综合考虑事务已执行时间、已消耗资源、重试次数和业务优先级,给每个事务计算一个成本分数,选择成本最低的事务回滚。
在分布式场景下,回滚操作本身也有代价。一个全局事务可能已经在多个节点上执行了大量写入,回滚时需要协调所有参与者执行逆向操作,期间还要处理部分节点已经提交、部分节点尚未提交的中间态。两阶段提交协议中的第二阶段,如果协调器在发送提交指令前崩溃,参与者就会陷入“不确定”状态,锁资源被长期持有,成为新的死锁隐患。这就要求死锁检测机制必须与事务管理器紧密配合,对于长时间处于 prepared 状态的事务,主动触发探活查询,超时后强制回滚或提交。
基于死锁的拒绝服务攻击原理攻击者利用分布式死锁进行拒绝服务攻击,手法远比想象中精细。最直接的方式是通过高频并发请求,故意在热点行上制造锁竞争。比如某个电商系统的库存扣减逻辑,攻击者用大量分布式事务同时锁定同一批商品行,每个事务还故意跨多个分片更新关联表,人为拉长事务持有锁的时间窗口。当全局等待图变得极度复杂时,死锁检测器本身的开销就会飙升,CPU 和网络带宽被大量消耗在检测而非业务处理上。
更高级的攻击会利用应用层的事务重试机制。很多中间件和 ORM 框架在捕获死锁异常后会自动重试,攻击者就刻意设计一种“伪死锁”模式:让事务 A 等待事务 B,事务 B 又等待事务 A,但两者持有的锁资源量不对称。死锁检测器回滚其中一个后,重试机制立刻重新发起,再次形成死锁,如此反复,数据库的吞吐量被这种“回滚-重试-再死锁”的循环迅速吞噬。攻击者甚至不需要很大的并发量,只要精确控制事务时序,就能用极低的成本造成显著影响。
锁超时与死锁检测的参数博弈分布式数据库通常提供两个关键参数来控制锁等待行为:锁等待超时和死锁检测间隔。锁等待超时设得太短,正常业务在高峰期容易误伤,大量事务因无法在超时时间内获取锁而被回滚;设得太长,死锁造成的资源占用时间变长,集群雪崩风险增加。死锁检测间隔则是检测算法的运行频率,间隔越短,死锁发现越快,但检测开销越大;间隔越长,系统能容忍的锁等待时间越长,但死锁持续时间也越长。
攻击者会针对这些参数进行探测和利用。通过逐步调整请求的并发度和事务复杂度,观察数据库的响应延迟和错误率变化,就能反推出目标系统的锁超时配置和检测周期。一旦摸清这些阈值,攻击者就可以设计出刚好卡在超时边界的事务模式,让正常业务请求被大量误杀,而自己的攻击事务却能通过重试机制持续占用资源。防御方需要引入自适应调整能力,根据当前系统的锁等待图复杂度和事务冲突率,动态调整检测频率和超时阈值,而不是依赖静态配置。
分布式锁服务引入的新攻击面很多分布式数据库依赖外部分布式锁服务来实现全局一致性,比如基于 etcd 或 ZooKeeper 的选主和锁管理。这类锁服务本身也会成为攻击目标。攻击者可以通过向分布式锁服务发起大量临时节点创建请求,耗尽存储空间或触发 watch 风暴,导致锁服务响应延迟急剧升高。数据库节点在等待锁服务响应时,本地事务持有的锁资源无法释放,连锁引发业务层面的死锁扩散。
另一种攻击手法是针对锁服务的网络分区。攻击者如果能够控制部分节点的网络连通性,就可以制造出经典的“脑裂”场景:被隔离的节点认为自己是主节点,继续持有锁并处理事务;而真正的主节点也在正常工作。两个主节点同时操作同一份数据,最终在数据合并时产生冲突,轻则事务失败,重则数据损坏。防御这类攻击需要分布式数据库在架构层面实现 fencing token 机制,每次锁服务变更时递增令牌,写入操作必须携带有效令牌,存储层校验令牌合法性,从而杜绝过期主节点的脏写。
乐观锁与悲观锁在防攻击上的取舍悲观锁模型下,事务在读取数据时就加锁,冲突在事前被阻止,但锁持有时间长,死锁概率高,正好给攻击者提供了制造等待图的原材料。乐观锁模型下,事务在提交时才检测冲突,锁持有时间极短,死锁几乎不会发生,但冲突检测失败后的回滚代价较高。攻击者针对乐观锁系统的策略是制造“写偏斜”场景,让大量并发事务读取相同数据集,各自基于读取结果做更新,提交时互相冲突,导致极高的回滚率和重试风暴。
混合策略正在成为主流。在热点数据上使用悲观锁,避免大量事务在提交阶段集中失败;在非热点数据上使用乐观锁,减少锁管理开销。但攻击者也会随之调整,他们会先通过采样式探测,找出系统中哪些数据路径走悲观锁,哪些走乐观锁,然后集中火力攻击悲观锁路径,制造死锁;或者对乐观锁路径发起写偏斜攻击,拉高整体回滚率。防御方需要具备热点自动识别和锁策略动态切换的能力,在检测到异常冲突率时,自动将乐观锁路径升级为悲观锁,或者对特定事务开启锁升级机制。
实际排查死锁的工程化方法当生产环境出现疑似死锁攻击时,第一优先级不是找到攻击源,而是快速止血。可以通过限流中间件对高冲突的事务模式进行熔断,临时将这些请求降级为串行化执行,或者直接拒绝并返回明确错误码,让客户端停止重试。同时,从数据库的慢查询日志和锁等待日志中提取事务指纹,找出参与死锁的事务特征,包括涉及的表、行范围、SQL 模式等。
以下是一段典型的从 TiDB 慢日志中提取死锁信息的分析脚本思路:
# 从 tidb_slow_query 表中查询死锁相关记录
SELECT
Time,
Query,
User,
Host,
(CASE
WHEN Query LIKE '%deadlock%' THEN 'DEADLOCK'
WHEN Query LIKE '%Lock wait timeout%' THEN 'LOCK_TIMEOUT'
ELSE 'NORMAL'
END) AS error_type,
Parse_Time,
Process_Time,
Wait_Time,
Txn_start_ts
FROM information_schema.cluster_slow_query
WHERE Time > NOW() - INTERVAL 1 HOUR
AND (Query LIKE '%deadlock%' OR Query LIKE '%Lock wait timeout%')
ORDER BY Time DESC
LIMIT 1000;
拿到这些数据后,需要还原出事务的调用链路。重点看 Txn_start_ts 接近的事务是否在短时间内大量出现死锁,以及它们的 Host 字段是否集中在少数客户端 IP。如果发现某个 IP 的事务启动时间戳密集且规律性极强,很大概率是自动化攻击脚本。接下来可以在数据库代理层或防火墙层对该 IP 的特定 SQL 模式进行限流,同时通知业务侧检查该来源的合法性。
长期防御体系建设单靠数据库自身的死锁检测机制无法完全抵御针对性攻击,需要建立多层防御体系。在接入层部署 SQL 防火墙,对事务的锁持有时间、跨分片数量、返回行数等维度进行实时统计,超出阈值的会话主动 kill。在中间件层实现事务管理策略,强制限制单个事务的最大操作数和执行时长,防止攻击者构造超长事务。在数据库内核层增强死锁检测的可观测性,将等待图的变化历史、死锁环的参与事务信息以结构化日志形式输出,供安全分析平台消费。
业务代码层面同样需要遵循防御性编程规范。访问数据时遵循统一的加锁顺序,比如所有涉及用户表和订单表的事务,必须先锁用户行再锁订单行,从根源上杜绝循环等待的可能。使用 SELECT ... FOR UPDATE 时,尽量带上精确的主键条件,避免间隙锁范围过大引入不必要的锁竞争。对于必须跨多个微服务调用的长事务链路,考虑改用 Saga 模式或 TCC 模式,将一个大事务拆分为多个短事务,每个短事务独立提交,通过补偿机制处理异常,大幅降低数据库层面的死锁概率。
分布式数据库的死锁治理,本质上是在系统吞吐量、延迟和一致性之间寻找动态平衡。攻击者的存在让这个平衡变得更加脆弱,但也促使整个行业在死锁检测算法、自适应参数调优、攻击面收敛等方面持续演进。理解死锁的形成机理和攻击手法,不是为了追求零死锁的理想状态,而是为了在死锁发生时,能够快速识别、精准止损,并持续加固系统的韧性。
