1. 当数据库"堵车"了:认识死锁的本质
就像早高峰两辆车在十字路口互不相让,MySQL死锁就是两个事务互相持有对方需要的锁资源。某天凌晨2点,我们的电商系统突然出现订单支付失败报警,日志里赫然躺着:
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
这就是典型的死锁报错。想象两个用户同时操作购物车:用户A先锁定了商品库存,用户B锁定了优惠券,然后他们都试图获取对方的资源,就像两个固执的司机谁也不肯倒车。
2. 死锁现场处理三板斧
2.1 事务回滚:时光倒流术
MySQL内置的死锁检测机制就像交通协管员,会强制回滚其中一个事务。以下是一个Java Spring Boot示例:
@Service
public class OrderService {
@Transactional
public void createOrder(OrderDTO order) {
try {
// 1. 扣减库存(先获取行锁)
inventoryMapper.decrease(order.getSkuId(), order.getQuantity());
// 2. 使用优惠券(可能产生第二个锁)
couponMapper.useCoupon(order.getUserId(), order.getCouponId());
// 3. 生成订单记录
orderMapper.insert(order);
} catch (DataAccessException e) {
if (e.getCause() instanceof MySQLTransactionRollbackException) {
// 这里会触发事务自动回滚
log.warn("检测到死锁,即将重试");
}
throw e;
}
}
}
这段代码演示了典型的事务边界处理。当死锁发生时,Spring的@Transactional
注解会自动触发回滚,就像把这段操作录像倒带,所有数据库修改都会被撤销。
2.2 重试机制:换个车道再出发
智能重试就像导航软件自动规划新路线。我们给上面的服务加上重试逻辑:
@Retryable(value = {DeadlockLoserDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 100))
public void createOrderWithRetry(OrderDTO order) {
createOrder(order);
}
这里使用Spring Retry实现:
maxAttempts=3
:最多重试3次backoff=100ms
:首次失败后等待100ms- 仅捕获死锁异常,避免无限重试
2.3 业务补偿:交警的人工疏导
对于关键业务,还需要准备补偿机制。比如支付系统的自动对账:
-- 每日凌晨执行的核对SQL
SELECT
o.order_no,
p.amount
FROM orders o
LEFT JOIN payment p ON o.order_no = p.order_no
WHERE
o.status = 'PAID'
AND p.id IS NULL;
这个查询会找出已标记支付但实际未成功的订单,触发人工或自动处理流程。
3. 技术选型中的平衡艺术
3.1 适用场景
- 电商秒杀:短事务+乐观锁+有限重试
- 银行转账:长事务+悲观锁+人工核查
- 社交动态:最终一致性+消息队列
3.2 方案对比
方案 | 响应时间 | 数据一致性 | 实现复杂度 |
---|---|---|---|
直接重试 | 快 | 低 | 简单 |
队列消峰 | 慢 | 最终一致 | 中等 |
人工干预 | 非常慢 | 高 | 复杂 |
3.3 避坑指南
- 重试次数不是越多越好,超过3次可能引发雪崩
- 等待时间要随机化,避免集体重试造成二次死锁
- 必须记录重试日志,方便后续分析
- 更新操作尽量按固定顺序(例如按ID升序)
4. 从车祸现场到秋名山车神
某互联网金融平台在实施以下优化后,死锁率下降87%:
- 所有更新操作按"账户ID升序"执行
- 将事务超时时间从默认50s改为3s
- 为高频查询字段增加组合索引
优化后的转账操作示例:
public void transfer(Long from, Long to, BigDecimal amount) {
// 按ID顺序锁定账户
List<Long> sortedIds = Arrays.asList(from, to).stream()
.sorted()
.collect(Collectors.toList());
accountMapper.lockAccount(sortedIds.get(0));
accountMapper.lockAccount(sortedIds.get(1));
// 执行转账逻辑...
}
这种排序加锁法就像要求所有车辆靠右行驶,从根本上避免了资源竞争乱序。
5. 老司机经验总结
经过多年与死锁的较量,我总结出三个核心原则:
- 快进快出:事务尽量简短,像进出便利店一样快速
- 规矩做人:统一操作顺序,就像排队结账不插队
- 留好后路:完备的日志和补偿机制,像买车险一样重要
下次当你看到死锁报错时,记住这不是世界末日,而是数据库在提醒:该优化你的"交通系统"了!通过合理的事务设计、智能重试和监控告警,我们完全可以让系统在并发洪流中优雅起舞。