issue-pos-order-lock-timeout.md 6.8 KB

pos_order 行锁等待超时(Lock wait timeout exceeded)排查记录

  • 日期: 2026-07-23 16:32
  • 环境: 测试环境(test 分支,端口 8082)
  • 接口: GET /system/orderShOprate/completeOrder(商家完成订单:state 2→3,payStatus 0→1)
  • 报错位置: PosOrderShOprateController.java:305
  • 涉及表: pos_order

一、报错信息(原文)

16:32:02.519 [http-nio-8082-exec-86] ERROR c.r.f.w.e.GlobalExceptionHandler - [handleRuntimeException,69] -
请求地址'/system/orderShOprate/completeOrder',发生未知异常.

org.springframework.dao.CannotAcquireLockException:
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:
Lock wait timeout exceeded; try restarting transaction
### The error may exist in com/ruoyi/system/mapper/PosOrderMapper.java (best guess)
### The error may involve com.ruoyi.system.mapper.PosOrderMapper.updateById-Inline
### The error occurred while setting parameters
### SQL: UPDATE pos_order SET state=?, pay_status=? WHERE id=?
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:
Lock wait timeout exceeded; try restarting transaction
; Lock wait timeout exceeded; try restarting transaction
    ...
    at com.baomidou.mybatisplus.extension.repository.AbstractRepository.saveOrUpdate(AbstractRepository.java:73)
    ...
    at com.ruoyi.system.service.impl.PosOrderServiceImpl$$SpringCGLIB$$0.saveOrUpdate(<generated>)
    at com.ruoyi.app.order.PosOrderShOprateController.completeOrder(PosOrderShOprateController.java:305)
    ...
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:
Lock wait timeout exceeded; try restarting transaction

(上方省略部分为 Tomcat / Spring Security 过滤器链与 MyBatis-Plus 反射代理帧,与根因无关。)


二、结论:这个报错是什么意思

核心:MySQL 行锁等待超时(错误码 1205,Lock wait timeout exceeded)。

不是 SQL 写错,也不是数据问题,而是:

这条 UPDATE pos_order ... WHERE id=? 想给某一行加排他锁(X 锁),但这一行的锁已经被另一个事务占着不放,当前事务等了超过 innodb_lock_wait_timeout(默认 50 秒)还没拿到,于是 MySQL 主动放弃并回滚这一条语句。

与死锁的区别

错误 含义 触发方式
1205 Lock wait timeout(本次) 别人占锁太久,我等不下去 等 ≥50s 后超时
1213 Deadlock 互相等对方,死锁 MySQL 检测到后立刻回滚一方

→ 本次是"另一个事务长时间没提交/回滚,把这一行锁住了",不是死锁


三、发生点代码

PosOrderShOprateController#completeOrder(第 292–313 行):

@Transactional(rollbackFor = Exception.class)   // 整个方法在一个事务里
@GetMapping("/completeOrder")
public AjaxResult completeOrder(@RequestHeader String token, @RequestParam Long id) {
    PosOrder order = posOrderService.getOne(...);
    // 状态/类型校验 ...
    PosOrder update = new PosOrder();
    update.setId(order.getId());
    update.setState(3L);
    update.setPayStatus(1L);
    posOrderService.saveOrUpdate(update);        // ← 305 行:拿 pos_order 行锁超时(受害者)
    ...
    orderLogHelper.log(...);
    PosOrder fullOrder = posOrderService.getById(order.getId());
    orderService.setSanghuBilling(fullOrder);   // 事务尚未提交,锁继续持有到方法结束
    return AjaxResult.success();
}

当前 completeOrder 是"被阻塞、最终超时"的受害者。真正的"凶手"是另一个占着同一行 pos_order 锁超过 50 秒的事务。


四、凶手最可能是谁(按概率排序)

  1. 某个卡死 / 未提交的事务长期持锁(测试环境最常见)

    • 之前有个请求(如支付回调 PayController.payipnNewebpayPayController、或另一次订单操作)也在 @Transactional 里改这一行,但中间卡住了(HTTP 推送超时、远程调用 hang 住、连接泄漏),事务一直没结束 → 锁不释放。
    • 或有人在 SQL 客户端里 BEGIN; UPDATE pos_order ... 后忘了 COMMIT,把行锁攥在手里。
  2. 同一订单的并发操作撞车

    • 前端重复点击"完成",或同一订单同时有"完成 + 取消 + 支付回调"在跑,几个事务抢同一行的锁。一般几秒内解决,不至于 50 秒;若超过 50 秒,多半叠加了原因 1。
  3. 长事务包裹了慢操作(系统性根因)

    • completeOrder@Transactional 内、update 后又调 setSanghuBilling(...) 生成账单。锁从 305 行 update 一直持有到方法结束才提交。若 setSanghuBilling 很慢(多表查询/计算),这个事务持锁就很久,反过来会成为"下一个并发请求"的凶手。
    • @Transactional 里混入慢/外部操作是这类问题的系统性根因。

五、如何定位真正的凶手

复现或刚好发生时,在 MySQL 里按顺序执行:

-- 1. 看当前所有活跃事务,重点看 trx_started 跑了多久、trx_state 是否 LOCK WAIT
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.INNODB_TRX;

-- 2. 看谁在等锁、谁堵谁(blocker vs waiter)
SELECT * FROM sys.innodb_lock_waits;

-- 3. MySQL 8:看 pos_order 上的锁持有者
SELECT * FROM performance_schema.data_locks
WHERE OBJECT_NAME = 'pos_order';

-- 4. 最近一次锁等待/死锁诊断
SHOW ENGINE INNODB STATUS\G   -- 看 TRANSACTIONS 段

判断要点:INNODB_TRXtrx_started 远早于现在、且 trx_query 为空的那个事务,基本就是占着锁不走的凶手。记下它的 trx_mysql_thread_id,可 KILL <id> 强制回滚释放锁(治标),再排查对应业务代码为何没提交(治本)。


六、处理方案

临时处理

找到持锁事务后 KILL <trx_mysql_thread_id>,订单即可正常完成。

长期改善(待实施)

  • 把慢操作(推送、账单计算、外部 HTTP 调用)移出 @Transactional,或用 TransactionTemplate 只包住真正需要的 DB 写入,缩短持锁时间。
  • 订单状态流转加幂等 / 状态机锁:UPDATE ... WHERE id=? AND state=2(乐观条件更新),避免并发撞车。
  • 排查支付回调链路(PayController.payipn / NewebpayPayController)是否存在长事务持锁。

七、关联

  • CLAUDE.md「已废弃代码清单」:在用的订单操作入口为 PosOrderShOprateController(商家)/ PosOrderQsOprateController(骑手)/ UserOrderController(用户)/ NewebpayPayController(支付)。
  • 支付链路复用 PayController.sendAcceptRiderPush,支付回调若在同一事务内更新 pos_order 可能成为持锁来源,排查时重点关注。