You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程调用同一段MySQL代码引发死锁问题排查求助

问题分析与解决方案

一、死锁的核心原因

你遇到的死锁,根本问题出在Universe行的锁升级冲突,同时你对SERIALIZABLE隔离级别的理解存在偏差:

纠正SERIALIZABLE的误解

SERIALIZABLE保证的是事务执行结果等价于串行执行,但它不会强制事务按固定顺序获取锁。它依赖范围锁/共享锁实现隔离,但如果事务的锁获取逻辑形成循环等待,死锁依然会发生。

具体死锁场景

你的每个事务执行流程是:

  1. 锁定自身User行(排他锁)
  2. 查询同一个Universe行(SERIALIZABLE下普通查询会加共享锁)
  3. 尝试更新Universe行(需要将共享锁升级为排他锁)

当多个并发事务同时执行到步骤2时,都会持有Universe行的共享锁。等到步骤3要升级排他锁时,每个事务都在等待其他事务释放共享锁,形成循环等待——这就是死锁的由来。

另外,updatePotSize和updatePayout采用的"先查询再更新"两步操作,会放大锁冲突概率:findOne加共享锁,update要升级排他锁,中间的时间窗口足够其他事务也加上共享锁。

二、死锁调试技巧

  1. 查看数据库死锁日志
    • 对于MySQL InnoDB,执行SHOW ENGINE INNODB STATUS;,输出里的LATEST DETECTED DEADLOCK段会显示死锁的事务ID、锁类型、等待链,精准定位冲突的行和锁。
  2. 添加锁流程日志
    • 在关键锁操作(比如getBuyingPower、updatePotSize)前后打印日志,记录事务ID、操作表、行ID、时间戳,直观看到锁的获取顺序和冲突点。
  3. 监控锁状态
    • 用数据库自带工具(比如MySQL的INFORMATION_SCHEMA.INNODB_LOCKS和INNODB_LOCK_WAITS表)实时查看当前持有和等待的锁。

三、具体修复方案

1. 将Universe的更新改为原子操作

把updatePotSize和updatePayout里的"查询+更新"两步合并成单条原子更新语句,避免共享锁的获取和升级:

// 重构updatePotSize
const updatePotSize = async (universeId, transaction) => {
  try {
    await Universes.update(
      { potSize: sequelize.literal('potSize + 1') },
      { where: { id: universeId }, transaction }
    );
  } catch (error) {
    console.error('Unable to update pot size:', error.message);
    throw error;
  }
};

// 重构updatePayout
const updatePayout = async (universeId, amount, transaction) => {
  try {
    if (isNaN(amount) || amount === null) {
      throw new Error('Invalid amount value');
    }

    await Universes.update(
      { payout: sequelize.literal('payout + ' + amount) },
      { where: { id: universeId }, transaction }
    );

    // 如需返回新值,直接加排他锁查询
    const updatedUniverse = await Universes.findOne({
      where: { id: universeId },
      attributes: ['payout'],
      transaction,
      lock: transaction.LOCK.UPDATE
    });
    return updatedUniverse.payout;
  } catch (error) {
    console.error('Unable to update payout:', error);
    throw error;
  }
};

原子更新会直接对目标行加排他锁,跳过共享锁步骤,从根源避免锁升级导致的死锁。

2. 调整锁获取顺序(可选)

如果原子更新还不够,强制所有事务先获取Universe的排他锁,再操作User行,确保锁获取顺序完全一致:

// 事务最开始先锁定Universe
await Universes.findOne({
  where: { id: universeId },
  transaction,
  lock: transaction.LOCK.UPDATE
});

// 再执行User相关操作
const buyingPower = await getBuyingPower(userId, transaction);
// ...后续逻辑

这种方式会降低并发度,但能彻底避免死锁,适合Universe更新频率极高的场景。

3. 优化重试机制

你现有的重试次数太少(3次),且要确保操作的幂等性:

  • 增加重试次数到5-10次,每次重试间隔100-500ms(指数退避)
  • 确保createStockOwner等写入操作幂等:比如给stockuuid加唯一键约束,重试时如果发现已存在则直接返回,避免重复创建

四、总结

SERIALIZABLE隔离级别不会自动解决死锁,它只是保证事务结果的串行一致性。死锁的本质是循环等待的锁依赖,解决核心是消除循环等待:要么统一锁获取顺序,要么用原子操作避免锁升级。

内容的提问来源于stack exchange,提问作者Marc-9

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 03:49:56