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 避坑指南

  1. 重试次数不是越多越好,超过3次可能引发雪崩
  2. 等待时间要随机化,避免集体重试造成二次死锁
  3. 必须记录重试日志,方便后续分析
  4. 更新操作尽量按固定顺序(例如按ID升序)

4. 从车祸现场到秋名山车神

某互联网金融平台在实施以下优化后,死锁率下降87%:

  1. 所有更新操作按"账户ID升序"执行
  2. 将事务超时时间从默认50s改为3s
  3. 为高频查询字段增加组合索引

优化后的转账操作示例:

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. 老司机经验总结

经过多年与死锁的较量,我总结出三个核心原则:

  1. 快进快出:事务尽量简短,像进出便利店一样快速
  2. 规矩做人:统一操作顺序,就像排队结账不插队
  3. 留好后路:完备的日志和补偿机制,像买车险一样重要

下次当你看到死锁报错时,记住这不是世界末日,而是数据库在提醒:该优化你的"交通系统"了!通过合理的事务设计、智能重试和监控告警,我们完全可以让系统在并发洪流中优雅起舞。