laravel日常使用的中间件

在以往的开发过程中遇到这样一个问题,用户下单后,后台要分配一个对接的业务人员二维码展示给用户。有一次业务人员反馈系统显示分配给了A业务人员他却加了B业务人员的微信。查看数据库发现,这个用户下了一个订单,同一个订单却有两条分配记录,而且是同一秒。很显然这是一个并发问题。

$orderTeacher = OrderTeacher::where('order_id', $orderId)->find();
if ($orderTeacher) {
    return $orderTeacher->qr_code;
}
$teacher = Teacher::find();//这里是实际的业务不详细描述
$orderTeacher = OrderTeacher::create([
    'order_id' => $orderId,
    'teacher_id' => $teacher['id'],
    'qr_code' => $teacher['qr_code'],
]);

return $teacher['qr_code'];

前端不知道发生了什么未知的原因,同一时刻请求了两次这个接口。

$orderTeacher = OrderTeacher::where('order_id', $orderId)->find();

两次请求这一段代码都没有查询到这个订单有分配记录,就走一下步创建分配记录返回二维码,前端收到了两次,用第二次收到的二维码替换了第一次。然后产生了一个订单分配两个业务人员的bug。
面对这类问题有很多结果方案,redis锁、给对应的mysql表加锁。这种做成中间件的方式是比较好的,可以把非业务逻辑的代码分离开。

class LockWaitMiddleware implements MiddlewareInterface
{
    public function handle(Request $request, \Closure $next, $scene = '', $maxLockTime = 5)
    {
        if ($request->isLogin()) {
            $uid = $request->uid();
        } else {
            $uid = $request->ip();
        }
        if ($scene === '') {
            $scene = $request->method() . ':' . $request->url();
        }
        $lockKey = "learnku:{$scene}wait_lock:{$uid}";
        $waitSecond = 0;
        $waitEveryTime = 0.2;
        while (true) {
            $acquired = CacheRedis()->exists($lockKey);
            if (!$acquired) {
                CacheRedis()->setex($lockKey, $maxLockTime, '1');
                break;
            }

            $waitSecond += $waitEveryTime;
            if ($waitSecond >= $maxLockTime) {
                break;
            }
            sleep($waitEveryTime);
        }

        $response = call_user_func($next, $request);

        CacheRedis()->del($lockKey);

        return $response;
    }
}

本来设置redis锁是用setnx + expire来实现

while(true) {
    $acquired = CacheRedis()->setnx($lockKey, 1);
    if ($acquired) {
        CacheRedis()->expire($lockKey, $maxLockTime);
        break;
    }
}

后来看到有人说如果setnx和expire这两个操作不是原子性的,如果程序只执行了setnx就报错了,那么这个值就永远存在redis不会过期了,所以改成第一种方式。这是一个环绕中间件,执行完业务代码后再删除键值

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 7
云客网络工作室

为啥不在数据库添加唯一索引嘞

1周前 评论
doublefacekill (楼主) 1周前

感觉用laravel缓存系统自带原子锁会好点

1周前 评论
陈先生

Cache::lock() ??

1周前 评论

同上,为什么使用Cache::lock()?锁因子一般跟随业务,不一定是用户ID或IP,不同业务对锁的控制时间可能不一致,把它放置于中间件,会将业务耦合在中间件,封装是个不错的想法,我觉得应该单独写个独立的SDK去做,业务层只作以回调方式处理。

1周前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!