GET /system/orderShOprate/completeOrder(商家完成订单:state 2→3,payStatus 0→1)PosOrderShOprateController.java:305pos_order16: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 秒的事务。
某个卡死 / 未提交的事务长期持锁(测试环境最常见)
PayController.payipn、NewebpayPayController、或另一次订单操作)也在 @Transactional 里改这一行,但中间卡住了(HTTP 推送超时、远程调用 hang 住、连接泄漏),事务一直没结束 → 锁不释放。BEGIN; UPDATE pos_order ... 后忘了 COMMIT,把行锁攥在手里。同一订单的并发操作撞车
长事务包裹了慢操作(系统性根因)
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_TRX 里 trx_started 远早于现在、且 trx_query 为空的那个事务,基本就是占着锁不走的凶手。记下它的 trx_mysql_thread_id,可 KILL <id> 强制回滚释放锁(治标),再排查对应业务代码为何没提交(治本)。
找到持锁事务后 KILL <trx_mysql_thread_id>,订单即可正常完成。
@Transactional,或用 TransactionTemplate 只包住真正需要的 DB 写入,缩短持锁时间。UPDATE ... WHERE id=? AND state=2(乐观条件更新),避免并发撞车。PayController.payipn / NewebpayPayController)是否存在长事务持锁。PosOrderShOprateController(商家)/ PosOrderQsOprateController(骑手)/ UserOrderController(用户)/ NewebpayPayController(支付)。PayController.sendAcceptRiderPush,支付回调若在同一事务内更新 pos_order 可能成为持锁来源,排查时重点关注。