1. 数据库死锁的本质与危害
死锁是数据库系统中两个或多个事务相互等待对方释放资源而导致的永久阻塞状态。想象两个人在狭窄的走廊相遇,都坚持让对方先走,结果谁都过不去——这就是死锁的经典场景。在MySQL中,当事务T1持有锁A并请求锁B,而事务T2同时持有锁B并请求锁A时,就会形成这种循环等待。
死锁的四大必要条件:
- 互斥条件:资源一次只能被一个事务占用
- 占有且等待:事务持有资源的同时请求新资源
- 不可剥夺:已分配的资源不能被强制收回
- 循环等待:存在事务之间的环形等待链
实际生产中最常见的死锁场景是不同事务以相反顺序访问相同的多行记录。比如订单系统中,事务A先更新订单表再更新支付表,而事务B则采用相反顺序。
死锁会导致:
- 系统吞吐量急剧下降
- 用户体验恶化(请求长时间无响应)
- 可能引发雪崩效应(连接池耗尽)
2. MySQL死锁检测机制深度解析
2.1 等待图算法实现
MySQL采用有向图检测死锁,这种算法的时间复杂度为O(n^2),其中n是活跃事务数。具体实现上:
-
构建等待图:
- 顶点:当前活跃事务
- 边:事务A等待事务B释放资源
-
环检测流程:
sql复制-- 伪代码表示检测逻辑
function detect_deadlock():
for each transaction in active_transactions:
if dfs(transaction, visited=[]):
return true
return false
function dfs(transaction, visited):
if transaction in visited:
return true
visited.append(transaction)
for each waiting_on in transaction.waiting_for:
if dfs(waiting_on, visited.copy()):
return true
return false
2.2 检测触发时机
MySQL在以下情况会启动死锁检测:
- 事务请求锁超时(innodb_lock_wait_timeout,默认50秒)
- 新锁请求需要等待时
- 周期性后台检测(间隔由innodb_deadlock_detect_interval控制)
在MySQL 8.0+中,可以通过设置innodb_deadlock_detect=OFF禁用死锁检测,但这会导致系统在死锁时完全挂起,仅适用于特定场景。
3. 自动回滚的决策逻辑
3.1 牺牲者选择策略
MySQL选择回滚事务时考虑的因素:
- 事务已修改的行数(修改越少代价越小)
- 事务已执行的时间(短事务优先保留)
- 事务的隔离级别(REPEATABLE READ比READ COMMITTED优先级高)
- 是否显式事务(START TRANSACTION比自动提交的事务优先级低)
具体选择算法:
python复制# 简化版的选择逻辑
def select_victim(transactions):
victim = None
min_cost = float('inf')
for txn in transactions:
cost = txn.modified_rows * 0.6 + txn.duration * 0.4
if cost < min_cost:
min_cost = cost
victim = txn
return victim
3.2 回滚执行过程
回滚的具体步骤:
- 获取事务的undo日志
- 逆向执行所有修改操作
- 释放所有持有的锁
- 记录死锁信息到错误日志
- 向客户端返回1213错误码(ER_LOCK_DEADLOCK)
典型错误日志示例:
code复制2023-08-20T14:23:56.771234Z 12 [Note] InnoDB:
Transactions deadlock detected, dumping detailed information.
2023-08-20T14:23:56.771245Z 12 [Note] InnoDB:
*** (1) TRANSACTION:
TRANSACTION 4221, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 12, OS thread handle 139887582312192, query id 142 localhost root updating
UPDATE accounts SET balance = balance - 100 WHERE id = 1
2023-08-20T14:23:56.771251Z 12 [Note] InnoDB:
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 27 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 4221 lock_mode X locks rec but not gap
4. 生产环境优化实践
4.1 死锁监控方案
推荐监控指标:
- 死锁发生率:SHOW STATUS LIKE 'innodb_row_lock%'
- 锁等待时间:performance_schema.events_waits_current
- 事务持续时间:information_schema.innodb_trx
监控脚本示例:
bash复制#!/bin/bash
while true; do
deadlocks=$(mysql -e "SHOW ENGINE INNODB STATUS" | grep -c "DEADLOCK")
echo "$(date +%FT%T),$deadlocks" >> deadlock_monitor.csv
sleep 60
done
4.2 应用层防护措施
- 访问顺序规范化:
java复制// 错误的写法 - 可能导致死锁
public void transfer(Account from, Account to, BigDecimal amount) {
synchronized(from) {
synchronized(to) {
// 转账逻辑
}
}
}
// 正确的写法 - 按固定顺序锁定
public void transfer(Account from, Account to, BigDecimal amount) {
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized(first) {
synchronized(second) {
// 转账逻辑
}
}
}
- 事务拆解技巧:
- 大事务拆分为小事务
- 热点数据最后更新
- 非必要读操作移出事务
5. 高频面试问题解析
5.1 经典死锁场景分析
场景一:交叉更新
sql复制-- 事务1
BEGIN;
UPDATE users SET score = score + 10 WHERE id = 1;
UPDATE users SET score = score - 5 WHERE id = 2;
COMMIT;
-- 事务2
BEGIN;
UPDATE users SET score = score + 8 WHERE id = 2;
UPDATE users SET score = score - 3 WHERE id = 1;
COMMIT;
解决方案:
- 按固定顺序更新(如先更新id小的记录)
- 使用SELECT...FOR UPDATE提前锁定所有记录
5.2 如何减少死锁发生
-
事务设计原则:
- 保持事务简短(理想情况下<100ms)
- 避免事务中包含用户交互
- 批量操作使用LIMIT分片
-
索引优化建议:
- 确保查询使用合适的索引
- 避免全表扫描的UPDATE/DELETE
- 复合索引字段顺序与查询条件一致
-
参数调优:
ini复制# my.cnf优化建议
[mysqld]
innodb_lock_wait_timeout=10 # 缩短锁等待超时
innodb_deadlock_detect=ON # 确保死锁检测开启
transaction_isolation=READ-COMMITTED # 降低隔离级别
6. 高级话题:分布式系统死锁处理
在微服务架构下,传统的数据库死锁检测机制不再适用。此时需要:
- 采用Saga模式:
mermaid复制graph LR
A[服务A:扣库存] --> B[服务B:创建订单]
B --> C{检查}
C -->|成功| D[服务C:扣款]
C -->|失败| E[补偿A]
- 使用分布式锁超时:
java复制// Redisson实现示例
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
try {
boolean acquired = lock1.tryLock(100, 10000, TimeUnit.MILLISECONDS);
if (acquired) {
boolean acquired2 = lock2.tryLock(100, 10000, TimeUnit.MILLISECONDS);
if (acquired2) {
// 业务逻辑
}
}
} finally {
lock2.unlock();
lock1.unlock();
}
- 定时任务扫描僵尸事务:
python复制def check_zombie_transactions():
expired = Transaction.objects.filter(
status='RUNNING',
updated_at__lt=timezone.now()-timedelta(minutes=5)
)
for txn in expired:
txn.rollback()
log_rollback_event(txn)
在实际项目中,我们曾遇到一个典型的死锁案例:账单生成服务与支付回调服务频繁发生死锁。通过分析发现两个服务都以不同的顺序更新相同的五张关联表。解决方案是制定了全局的《数据更新顺序规范》,强制所有服务按照account→order→payment→log→audit的顺序操作表,彻底解决了这类死锁问题。
