前提提要
今天产品端提了个需求,院校组要求借调我去帮忙,因为我以前做过商城,现在他们需求做一个积分商城,需要做一个秒杀模块,结果毫无意外的我被借调过去了,刚好可以复习一下以前的知识,现在介绍一下redis的秒杀机制,首先大家需要了解redis的基础信息,可以参考小编的前面的文章 https://blog.csdn.net/masterphp/article/details/130728703,现在开始介绍redis秒杀,只要有两种方式来实现,锁和队列
秒杀流程概述
在进行Redis的实现之前,我们先简要介绍一下秒杀的流程:
1.用户在前端页面选择要秒杀的商品,并提交订单;
2.系统校验用户提交的订单是否合法,如商品库存是否充足、用户是否符合参加条件等;
3.系统将订单信息写入数据库,并返回给用户订单处理中的状态;
4.用户在订单处理中不断轮询订单状态,直到订单完成。
在秒杀流程中,系统需要进行多次校验和处理,其中最关键的部分就是判断商品库存是否充足,这部分需要保证数据的可靠性和并发性。
秒杀实现原理
1.使用Redis队列存储订单:秒杀活动中,会有大量的订单请求同时涌入系统,若采用传统的关系型数据库进行存储,会导致并发量过大,数据库连接数量过多,从而降低系统的性能和稳定性。为了解决这个问题,我们可以使用队列来存储订单信息。Redis中提供了List类型的数据结构,可用作队列的实现。通过将订单信息放入Redis队列中,系统可以快速处理大量的订单请求,并以先到先服务的顺序进行处理。
2.使用Redis预减库存:秒杀活动中,需要对商品库存进行实时监控,否则会因为库存不够而导致订单处理失败。但是,如果每个订单都从数据库中查询商品库存,将会增加数据库的压力,导致系统并发性能降低。
为了解决这个问题,我们可以使用Redis的预减库存策略。当用户抢购时,我们先使用Redis缓存中的商品库存信息,并实时更新该缓存信息,如果库存已经减少至0,则直接返回秒杀失败。
3.使用Redis分布式锁:在秒杀活动中,虽然我们使用Redis队列和预减库存,但是系统仍然需要面对大量的并发请求。在这些请求中,可能会有多个用户同时对同一个商品进行抢购,这就需要我们使用分布式锁来保证数据的可靠性。Redis的分布式锁使用方式非常简单,在抢购开始时,我们使用Redis的SETNX操作来尝试获取分布式锁,如果获取成功,则对库存进行操作,操作完成后,释放分布式锁。如果获取分布式锁失败,则需等待一段时间重试。
乐观锁实现方式
//添加商品库存
public function addGoodsStock(Request $request)
{
//接受数据
$goods_id = $request->input('goods_id');
$store = $request->input('store');
//设置商品库存的key
key = 'seckill_goods_id_'.goods_id;
res = Redis::set(key,$store);
return $res;
}
//用户秒杀商品
public function buy(Request $request)
{
//接受数据
$uid = $request->input('uid');
$goods_id = $request->input('goods_id');
//商品库存key
key = 'seckill_goods_id_'.goods_id;
//监听对应的key,事务提交之前,如果key被修改,则事务被打断
Redis::watch($key);
//获取商品库存
store = Redis::get(key);
//抢购成功用户集合key
$setKey = 'userGoodsSuccess';
//判断该用户是否已经抢购过(该用户id是否在抢购用户集合中)
userBuyStatus = Redis::sismember(setKey,$uid);
if($userBuyStatus){
return '您已抢过!';
}
if($store){
//记录用户信息,更新库存(保证这一组命令,要么全部成功,要么都不成功)
Redis::multi();//开始事务
Redis::decr($key);//减少库存
//将用户id添加到抢购成功用户集合中
Redis::sadd(setKey,uid);
$result = Redis::exec();//提交,判断当前的key是否被某个客户端修改了
//结果判断
if($result){
//操作数据库,修改商品库存销量
DB::table('goods')->where('id', $goods_id)->decrement('stock', 1, ['sale' => DB::raw('`sale`+1')]);
//创建订单信息
return '抢购成功!';
}else{
return '抢购失败,请重试!';
}
}else{
return '已抢光!';
}
}
队列实现方式
//初始化库存队列
public function init(Request $request)
{
//接受参数
$goods_id = $request->input('goods_id');
$store = $request->input('store');
//获取key
key = 'list_seckill_goods_id_'.goods_id;
//防止对已经设置过的商品库存进行覆盖
if(!empty(Redis::llen($key))) {
return '已经设置了库存';
}
//初始化缓存,删除抢购用户id队列key和成功信息保存key,这个是抢购时生成的,防止错乱,初始化删除
userListKey = 'user_goods_id_'.goods_id;
Redis::command('del', [$userListKey, 'success']);
// 将商品存入Redis链表中
for($i = 1; $i <= $store; $i++) {
Redis::lpush($key, $i);
}
//设置过期时间
Redis::expire($key, 120);
echo '商品存入队列成功,数量:'.Redis::llen($key);
}
//redis队列抢购
public function start(Request $request)
{
// 模拟随机登录用户
$uid = mt_rand(1, 9999);
//$uid = $request->input('uid');
$goods_id = $request->input('goods_id');
//获取key
key = 'list_seckill_goods_id_'.goods_id;
//从链表的头部删除一个元素,返回删除的元素,因为pop操作是原子性,即使很多用户同时到达,也是依次执行
count = Redis::lpop(key);
if (!$count) {
return '已抢光!';
}
//已抢购用户id队列
userListKey = 'user_goods_id_'.goods_id;
//判断该用户是否已经抢购过(该用户id是否在抢购用户集合中)
userBuyStatus = Redis::sismember(userListKey,$uid);
if($userBuyStatus){
return '您已抢过!';
}
//将用户id添加到抢购成功用户集合中
Redis::sadd(userListKey,uid);
msg = '抢到的人为:'.uid.',抢到商品的顺序为第'.$count.'个';
Redis::lpush('success', $msg);
//操作数据库,修改商品库存销量
DB::table('goods')->where('id', $goods_id)->decrement('stock', 1, ['sale' => DB::raw('`sale`+1')]);
//创建订单信息
return '恭喜您抢购成功!';
}