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 协议》,转载必须注明作者和本文链接
为啥不在数据库添加唯一索引嘞
感觉用laravel缓存系统自带原子锁会好点
Cache::lock()
??同上,为什么使用
Cache::lock()
?锁因子一般跟随业务,不一定是用户ID或IP,不同业务对锁的控制时间可能不一致,把它放置于中间件,会将业务耦合在中间件,封装是个不错的想法,我觉得应该单独写个独立的SDK去做,业务层只作以回调方式处理。