# 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() 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 行): ```java @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.payipn`、`NewebpayPayController`、或另一次订单操作)也在 `@Transactional` 里改这一行,但中间卡住了(HTTP 推送超时、远程调用 hang 住、连接泄漏),事务一直没结束 → 锁不释放。 - 或有人在 SQL 客户端里 `BEGIN; UPDATE pos_order ...` 后忘了 `COMMIT`,把行锁攥在手里。 2. **同一订单的并发操作撞车** - 前端重复点击"完成",或同一订单同时有"完成 + 取消 + 支付回调"在跑,几个事务抢同一行的锁。一般几秒内解决,不至于 50 秒;若超过 50 秒,多半叠加了原因 1。 3. **长事务包裹了慢操作**(系统性根因) - `completeOrder` 在 `@Transactional` 内、update 后又调 `setSanghuBilling(...)` 生成账单。**锁从 305 行 update 一直持有到方法结束才提交**。若 `setSanghuBilling` 很慢(多表查询/计算),这个事务持锁就很久,反过来会成为"下一个并发请求"的凶手。 - `@Transactional` 里混入慢/外部操作是这类问题的系统性根因。 --- ## 五、如何定位真正的凶手 复现或刚好发生时,在 MySQL 里按顺序执行: ```sql -- 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_TRX` 里 `trx_started` 远早于现在、且 `trx_query` 为空的那个事务,基本就是占着锁不走的凶手。记下它的 `trx_mysql_thread_id`,可 `KILL ` 强制回滚释放锁(治标),再排查对应业务代码为何没提交(治本)。 --- ## 六、处理方案 ### 临时处理 找到持锁事务后 `KILL `,订单即可正常完成。 ### 长期改善(待实施) - [ ] 把慢操作(推送、账单计算、外部 HTTP 调用)移出 `@Transactional`,或用 `TransactionTemplate` 只包住真正需要的 DB 写入,缩短持锁时间。 - [ ] 订单状态流转加幂等 / 状态机锁:`UPDATE ... WHERE id=? AND state=2`(乐观条件更新),避免并发撞车。 - [ ] 排查支付回调链路(`PayController.payipn` / `NewebpayPayController`)是否存在长事务持锁。 --- ## 七、关联 - CLAUDE.md「已废弃代码清单」:在用的订单操作入口为 `PosOrderShOprateController`(商家)/ `PosOrderQsOprateController`(骑手)/ `UserOrderController`(用户)/ `NewebpayPayController`(支付)。 - 支付链路复用 `PayController.sendAcceptRiderPush`,支付回调若在同一事务内更新 `pos_order` 可能成为持锁来源,排查时重点关注。