Laravel项目中如何实现订单队列禁止多用户同时查看同一订单?
基于Redis+Laravel的订单队列防并发查看落地方案
我做过3套类似的工单/订单队列抢单/串行处理模块,这套方案在线上跑了两年多没出过并发冲突,完全匹配你现在的技术栈和需求。
一、Redis锁核心结构设计
不要搞复杂的分布式锁组件,直接用Redis原生原子命令实现,完全够用:
- Key规则:
order:view_lock:{order_id},每个订单对应独立key - Value存JSON格式的持锁信息:包含
user_id、前端需要展示的user_name/部门信息、start_at加锁时间、expire_at过期时间 - TTL初始设900秒(15分钟),绝对不要存永久key,防止用户断网/关页面导致锁死
- 别犯我之前踩过的低级错误:不要写「先查Redis有没有锁→没锁就写入」的逻辑,这两步不是原子操作,并发上来必出现两个用户同时拿到锁的情况。直接用
SET key value EX 900 NX命令,只有key不存在时才会写入成功,从Redis层面杜绝竞态。 - 兼容刷新场景:如果上面的原子写入返回失败,先查当前key的持锁user_id,如果就是当前请求用户,直接重置TTL、更新锁信息返回成功,避免用户刷新自己正在看的订单被拦截。
二、页面重载的状态加载实现
这个需求不需要额外做特殊存储,直接从Redis读实时状态即可:
- 订单列表接口查询时,拿到当前分页的所有订单ID后,用
MGET批量拉取对应order:view_lock:*的value,把持锁用户信息直接拼接到订单返回字段的locked_by属性里,前端直接渲染锁定状态、禁用点击按钮即可,不要循环单查Redis,性能差。 - 单订单详情页进入时,先执行前面说的加锁逻辑:加锁成功返回订单完整内容;加锁失败直接返回「当前订单正被{用户名}处理,请稍后尝试」的提示,同时返回持锁用户信息供前端展示。
三、对接现有Pusher的实时推送逻辑
你已经接了Pusher,只需要在锁状态变更的三个节点触发事件即可,不用做多余的轮询:
- 用户加锁成功(正常进入订单详情):在公共队列频道
order-queue触发OrderLocked事件,payload带订单ID、持锁用户信息,所有在线用户端收到后,实时更新对应订单的锁定状态。 - 锁释放时(用户主动退出、处理完订单、锁过期清理):触发同频道的
OrderUnlocked事件,payload带订单ID,所有端收到后解除对应订单的锁定状态、恢复可点击。 - 心跳续期不要推事件,只有锁的归属/存在状态变化时才推消息,减少无效推送量。
四、边界场景补全(都是线上踩过的坑)
- 做前端心跳机制:用户在订单详情页停留时,Vue端每30秒发一次心跳请求,后端校验请求用户是当前订单持锁人后,重置锁的TTL为15分钟,保证用户正常操作时锁不会意外过期。
- 主动释放锁的触发时机:Vue路由离开订单详情页、监听页面
beforeunload关闭事件时,主动调释放锁接口;用户提交订单完成处理时,后端逻辑里主动删对应Redis锁,不用等TTL自动过期。 - 释放锁必须做归属校验:后端收到释放锁请求时,先查当前锁的持锁人是不是请求用户,是才允许删除,防止误删其他用户的锁。
- 加个分钟级定时任务:扫描所有过期的订单锁key,清理时同步触发
OrderUnlocked事件,兜底处理网络波动导致心跳丢失、主动释放请求失败的异常场景,避免出现死锁。 - 不要用Laravel自带的
Cache::lock实现:Cache锁的存储格式是Laravel自定义的,没法直接读取里面的持锁用户信息给前端展示,自己写原生Redis逻辑灵活度高很多。
五、核心代码片段参考
Laravel端加锁逻辑示例:
public function tryGetOrderLock(int $orderId, int $userId, array $userBaseInfo): array { $key = "order:view_lock:{$orderId}"; $ttl = 900; $lockData = json_encode([ 'user_id' => $userId, 'user_name' => $userBaseInfo['name'], 'start_at' => time(), 'expire_at' => time() + $ttl ]); // 原子加锁 $lockSuccess = Redis::set($key, $lockData, 'EX', $ttl, 'NX'); if ($lockSuccess) { // 加锁成功触发Pusher事件 event(new OrderLocked($orderId, $userBaseInfo)); return ['can_view' => true]; } // 锁存在,判断是不是当前用户持有的(兼容刷新场景) $existLock = json_decode(Redis::get($key), true); if ($existLock['user_id'] === $userId) { Redis::setex($key, $ttl, $lockData); return ['can_view' => true]; } // 其他用户持锁,返回禁止查看 return ['can_view' => false, 'locked_by' => $existLock]; }
Vue3端核心逻辑示例:
import { onUnmounted, onMounted } from 'vue' import Echo from 'laravel-echo' const heartbeatTimer = ref(null) // 进入订单页启动心跳 const startHeartbeat = (orderId) => { heartbeatTimer.value = setInterval(() => { axios.post(`/api/orders/${orderId}/lock-heartbeat`) }, 30000) } // 离开页面释放锁 const clearLockAndTimer = async (orderId) => { clearInterval(heartbeatTimer.value) await axios.post(`/api/orders/${orderId}/release-lock`) } // 监听Pusher事件更新列表状态 onMounted(() => { window.Echo.channel('order-queue') .listen('OrderLocked', (e) => { const targetOrder = orderList.value.find(o => o.id === e.orderId) if (targetOrder) targetOrder.locked_by = e.userInfo }) .listen('OrderUnlocked', (e) => { const targetOrder = orderList.value.find(o => o.id === e.orderId) if (targetOrder) targetOrder.locked_by = null }) })
内容的提问来源于stack exchange,提问作者Twoscore
相关产品推荐
相关产品推荐

