前情提要
前段时间帮客户做了一个线上会议网站,网站实际运营 2 个多月,正常参会用户注册量大概有1万多。
网站其中有两个模块一个是积分兑换,一个是大转盘抽奖。用户通过浏览网站指定的会议视频或赞助商信息后可获得积分,积分达到一定程度后可参与这两个模块的兑换或抽奖。
因为兑换的商品是人民币等值的京东卡(50元,100元,200元三个商品),可能会被刷,所以在程序开发前就考虑了如何不产生并发超卖的情况。
如何不发生超卖现象?
根据网站的注册数量以及实际兑换/抽奖需求进行评估之后,我们对这两个模块的开发初步制定了如下思路(因两个模块基本相同,以下仅以积分兑换模块作为说明)。
-
将给用户兑换的商品提前锁定库存并写入
redis
队列(此处为调度任务脚本自动执行) -
在符合兑换要求的用户进行兑换商品时,读取
redis
队列,并在下单时(兑换可以视为下订单流程)使用redis
锁避免mysql
的重复写入。
代码如何实现
根据上述已明确的思路,开发工作分为两个步骤进行。
1.事先将需要兑换商品及库存写入 redis
队列
<?php
// 以下仅给出核心代码,其他自定义判断可根据需求自行编写
// 查找商品及库存
$date = '2022-04-01'
$goods = Goods::query()->select(['id', 'goods_store'])->where('goods_date', $date)->get();
// 将查询到的商品及库存写入redis队列
if($goods ->isNotEmpty()){
foreach ($goods as $key =>$value){
$store = $value['goods_store'];
$llen = app('redis')->llen('goods_store:' . $value['id']);
$count = $store - $llen;
// 将每个商品的库存写入各自商品的key,写入的vaule是商品id,为了后续直接取出后使用该id来走下单流程
for($i = 0; $i < $count; $i++){
app('redis')->lpush('goods_store:' . $value['id'], $value['id']);
}
}
return;
}
2.实现用户积分兑换的下单流程,使用 redis
锁并写入数据库
<?php
// 其他积分兑换的条件判断省略
// 消费商品,从队列中取出商品
$count = app('redis')->lpop('goods_store:' . $goods_id);
// 使用兑换用户的id为key,使用redis加锁下单,redis锁有很多种实现方式,其他方式可自行实现
$lock = $this->lock($user->id);
if($lock){
// 这里是下单写表逻辑,$count变量其实就是兑换商品的id,详见上一步。下单写表代码省略。
$mysql_data = $this->storeOrder($user, $count);
if(!$mysql_data){
$this->unlock($user->id);
throw new Exception("error");
}else{
$this->unlock($user->id);
return $this->success();
}
}
Redis 锁的实现(悲观锁)
// 加锁,给锁设置过期时间, 避免锁释放失败导致死锁现象
public function lock($id){
return Redis::set("points:lock", $id, "nx", "ex", 10);
}
// 解锁,使用lua脚本减少网络开销,原子性操作等
public function unlock($id)
{
$script = <<<'LUA'
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
return Redis::eval($script, 1, "points:lock", intval($id));
}
结论
上述实现的防止商品超卖逻辑中 PHP
代码如果直接使用需在 Laravel
框架中运行, 其他框架或架构需简单修改后方可运行。
在上述逻辑实现用户兑换商品成功后,需要的做的还有很多,比如检查商品的正确性,减少商品在数据库中的库存,获取商品对应的奖品信息(这里是京东卡券),最后将奖券下发给用户。
这段代码最终的奖券邮件发送逻辑采用的是 Laravel
中的事件系统(队列)实现的,该实现步骤可大致参考我的另一篇文章:使用 Laravel 事件系统与消息通知实现给新注册用户发送通知邮件
同时,在使用上述代码运行积分兑换的一周多的时间里,积分有被刷的情况,但没有发生过一次超卖现象,证明该逻辑在一定流量的并发下对防止商品超卖的情况相对稳定有效。