高峰期下单、库存扣减或批量更新时,日志出现 SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock。死锁不是简单的“数据库太慢”,而是两个事务以不同顺序等待彼此持有的锁。MySQL 会回滚其中一个事务解除循环。

事故发生时先保存现场

mysql -e "SHOW ENGINE INNODB STATUS\G" > /tmp/innodb-status.txt
mysql -e "SELECT * FROM information_schema.innodb_trx\G" > /tmp/innodb-trx.txt
mysql -e "SHOW FULL PROCESSLIST" > /tmp/processlist.txt
grep -Rni 'Deadlock found|SQLSTATE[40001]' var/log | tail -n 100

SHOW ENGINE INNODB STATUS 只保留最近一次死锁,等故障过去再查往往已被覆盖。生产环境可开启适当的 deadlock 日志并配置轮转。

阅读 LATEST DETECTED DEADLOCK

  • 记录两个事务正在执行的 SQL;
  • 记录已持有和正在等待的 lock mode;
  • 记录涉及的索引、记录和修改行数;
  • 确认最终被回滚的是哪个事务;
  • 追到发起 SQL 的 Cron、Consumer、API 或后台操作。

仅凭表名停掉所有消费者会扩大影响。要从时间、线程和业务唯一键把数据库现场与 Magento 日志对应起来。

事务范围过大会放大冲突

$connection->beginTransaction();
try {
    $this->resourceA->save($a);
    // 数据库事务中不要调用外部 API 或处理大文件
    $this->resourceB->save($b);
    $connection->commit();
} catch (Throwable $e) {
    $connection->rollBack();
    throw $e;
}

把外部接口、邮件与大文件处理移出事务。多个业务路径更新同一组实体时统一顺序,例如总是先订单、再库存,避免另一条路径反向加锁。

缺少索引会锁住更多记录

EXPLAIN UPDATE custom_table SET status=1
WHERE external_id='abc' AND store_id=2;
SHOW INDEX FROM custom_table;

增加索引前先在预发布验证执行计划和写入成本,不要在高峰期直接 ALTER 大表。

有限重试必须以幂等为前提

for ($attempt = 1; $attempt <= 3; $attempt++) {
    try {
        $this->operation->execute($idempotencyKey);
        break;
    } catch (MagentoFrameworkDBAdapterDeadlockException $e) {
        if ($attempt === 3) { throw $e; }
        usleep(random_int(50000, 200000) * $attempt);
    }
}

纯内部状态更新可有限重试;已经调用支付、发券、发货或 ERP 的流程,必须先用唯一键确认外部动作是否发生。上线后同时观察死锁次数、事务耗时、队列重试和订单完整性,不能把异常吞掉或无限重试。

用业务键核对被回滚事务

数据库选择的 victim 可能是客户下单,也可能是后台批量任务。根据订单号、SKU、外部请求 ID 检查事务是否已经部分产生消息、历史记录或外部调用。若数据库回滚但消息已投递,就必须依赖消费者幂等性阻止重复处理。

对于周期性批量任务,可按主键排序并分小批提交,减少一次事务持有的锁数量,同时避免多个 worker 处理重叠范围。

把死锁频率与流量放在一起看

单次死锁可能是正常并发竞争,持续增长则需要工程修复。按分钟汇总错误次数,并与下单量、消费者并发数、批量任务开始时间和数据库 CPU/IO 对齐。若每次索引任务启动就出现同一组表冲突,可以调整任务窗口或拆分批次,但仍要统一代码的锁顺序。

grep 'Deadlock found' var/log/*.log | cut -c1-16 | sort | uniq -c | tail -n 30
mysql -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';"
mysql -e "SHOW GLOBAL STATUS LIKE 'Threads_running';"

修改后保留一段时间的基线数据,确认死锁下降不是因为吞吐也下降。性能优化必须同时观察成功订单数与处理延迟。