技术群内再度热议:"监控系统告警频发------数据库出现严重死锁警告,订单业务几近停滞。"
新入职的同事小王看到群内消息,心中不禁一沉:"这不是我刚接手的项目吗?我只是增加了几个简单的更新操作啊......"小王仔细查看群中反馈的监控截图与运行日志,在困惑之余更添几分忐忑与焦急,连忙向资深同事请教。老同事耐心解释道:"你这是不小心踩中了 MySQL 的死锁陷阱......"
何谓死锁?简而言之,死锁犹如两人相遇于独木桥:
甲自东向西,乙自西向东
两人行至桥中,互不相让
最终双双无法通过
在 MySQL 中,当两个或更多事务相互等待对方释放持有的锁时,便会形成死锁。
小王所遇的死锁场景:
sql
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
status VARCHAR(20),
version INT
);
CREATE TABLE account_balance (
user_id BIGINT PRIMARY KEY,
balance DECIMAL(10,2),
frozen_amount DECIMAL(10,2)
);
死锁代码示例
java
// 小王的业务逻辑------同时更新订单与账户
@Service
public class OrderService {
@Transactional
public void processOrder(Long orderId, Long userId) {
// 先更新订单状态
updateOrderStatus(orderId, "PROCESSING");
// 再冻结账户金额
freezeUserBalance(userId, 100.00);
}
@Transactional
public void cancelOrder(Long orderId, Long userId) {
// 先解冻账户金额
unfreezeUserBalance(userId, 100.00);
// 后更新订单状态
updateOrderStatus(orderId, "CANCELLED");
}
}
并发执行时的死锁场景:
时间线:
-
线程A:开始执行 `processOrder(1, 100)`
-
获取 `orders` 表中 `id=1` 的行锁
准备获取 `account_balance` 表中 `user_id=100` 的行锁
-
线程B:开始执行 `cancelOrder(2, 100)`
-
获取 `account_balance` 表中 `user_id=100` 的行锁
准备获取 `orders` 表中 `id=2` 的行锁
-
死锁发生!
-
线程A等待线程B释放 `account_balance` 的锁
线程B等待线程A释放 `orders` 的锁
相互等待,形成死锁
死锁带来的影响
-
业务停滞:相关订单无法继续处理
-
用户体验下降:用户面临操作超时
-
系统资源浪费:连接池占满,CPU 空转消耗
-
数据不一致风险:部分事务被迫回滚
如何有效避免死锁?
- 统一操作顺序
java
// 优化后:始终先操作订单表,再操作账户表
@Service
public class OrderService {
@Transactional
public void processOrder(Long orderId, Long userId) {
updateOrderStatus(orderId, "PROCESSING");
freezeUserBalance(userId, 100.00);
}
@Transactional
public void cancelOrder(Long orderId, Long userId) {
updateOrderStatus(orderId, "CANCELLED"); // 先操作订单表
unfreezeUserBalance(userId, 100.00); // 再操作账户表
}
}
- 设置事务超时
java
@Transactional(timeout = 3) // 设定事务超时时间
public void processOrderWithTimeout(Long orderId, Long userId) {
// 业务逻辑
}
- 降低事务粒度
java
// 将大事务拆分为小事务
public void processOrderInSmallTransactions(Long orderId, Long userId) {
updateOrderStatus(orderId, "PROCESSING");
// 账户操作置于独立事务中
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status > {
return freezeUserBalance(userId, 100.00);
});
}
- 采用乐观锁机制
sql
UPDATE account_balance
SET balance = balance 100, version = version + 1
WHERE user_id = 100 AND version = {oldVersion};
- 死锁检测与重试策略
java
@Slf4j
@Service
public class OrderService {
private static final int MAX_RETRY = 3;
public void processOrderWithRetry(Long orderId, Long userId) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
try {
processOrder(orderId, userId);
return; // 执行成功则退出
} catch (DeadlockLoserDataAccessException e) {
retryCount++;
log.warn("检测到死锁,正在进行第{}次重试", retryCount);
if (retryCount == MAX_RETRY) {
throw e;
}
// 采用指数退避策略
try {
Thread.sleep(100 * (long) Math.pow(2, retryCount));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw e;
}
}
}
}
}
死锁排查工具
- 查看死锁日志
sql
查看近期死锁信息
SHOW ENGINE INNODB STATUS;
开启死锁日志记录
SET GLOBAL innodb_print_all_deadlocks = ON;
- 监控锁状态
sql
查看当前锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
避免死锁的核心原则
-
保持一致的访问顺序:对多个资源按固定顺序进行访问
-
控制事务粒度:尽量使事务轻量化,缩短锁持有时间
-
合理设计索引:确保查询利用索引,减少锁竞争范围
-
设置事务超时:为事务配置合理的超时时间
-
实现重试机制:对可能引发死锁的操作引入重试逻辑
总结:防范优于补救。死锁虽是并发系统中的常见问题,但通过良好的架构设计与编码实践,完全能够规避其发生。在系统设计与编码阶段即充分考虑并发安全,方能保障系统持续稳定运行。