You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 01:18:21