redis秒杀
基于redis 实现并发情况的超卖超买解决方案
秒杀功能主要有两个问题要解决: ①高并发对数据库产生的压力 ②竞争状态下如何解决库存的正确减少(“超卖” 问题)。
第一个问题,对于 PHP 来说很简单,用缓存技术就可以缓解数据库压力,比如 memcache,redis 等缓存技术,这里我使用了 redis。 第二个问题,我使用 redis 队列,因为 pop 操作是原子的,即使有很多用户同时到达,也是依次执行。
具体方案:
- 将商品库存存入redis队列,并按照活动结束时间设置过期时间。
- 在用户抢购商品时range抢购成功的用户,判断是否重复抢购
- 将用户加入商品抢购队列中
- 防止抢购用户过多将redis服务器炸掉,可以设置判断条件,如并发抢购用户不可以大于该商品库存,或者设置最大容许并发几个用户抢购
- 判断redis队列中该商品库存是否足够
- 使用redis的pop删除一个元素,返回删除的元素,因为pop操作是原子性,即使很多用户同时到达,也是依次执行
- 加订单,减库存
- 将成功抢购该商品的用户push到成功抢购商品的队列中
- 使用ab测压测试抢购
展示:
执行商品队列初始化方法Initialization:
执行:
ab -c 20 -n 200 -T 'application/x-www-form-urlencoded' -p public/post.txt https://blogs.chencong.fun/start
(此为post方式ab测压)
数据库:
CREATE TABLE `job_goods` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '商品名称',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
`expired_at` timestamp NOT NULL DEFAULT '2021-12-17 08:26:34' COMMENT '过期时间',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态0禁用1启用',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
CREATE TABLE `job_order` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`order_sn` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单号',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
`goods_id` int(11) NOT NULL DEFAULT '0' COMMENT '商品id',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
代码:
<?php
namespace App\Http\Controllers;
use App\Models\Goods;
use App\Models\Order;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
class GoodsController extends Controller
{
/**
* @description redis生成指定商品key 存储所有商品
* @param Goods $model
* @return string
*/
public function prefix(Goods $model):string {
$goods_id='redis_goods_'.$model->id;
$redis_goods_list=Redis::lrange('redis_goods_list',0,100);
if (!in_array($goods_id, $redis_goods_list)) {
Redis::lpush('redis_goods_list',$goods_id);
Redis::expire('redis_goods_list', 30*24*60*60);
}
return $goods_id;
}
/**
* @description 成功抢购指定商品的用户
* @param Goods $model
* @return string
*/
public function goods_user_success(Goods $model):string {
$goods_user_success_id='goods_user_success'.$model->id;
return $goods_user_success_id;
}
/**
* @description 抢购指定商品的用户
* @param Goods $model
* @return string
*/
public function goods_user(Goods $model):string {
$goods_user_id='goods_user'.$model->id;
return $goods_user_id;
}
/**
* @description 商品redis初始化
*/
public function Initialization(){
$redis_goods_list= Redis::lrange('redis_goods_list',0,100);
foreach ($redis_goods_list as $list_item){
$redis_goods_list[]='goods_user_success'.str_replace('redis_goods_','',$list_item);
$redis_goods_list[]='goods_user'.str_replace('redis_goods_','',$list_item);
}
$redis_goods_list[]='redis_goods_list';
// 初始化
Redis::command('del',$redis_goods_list);
$goods=Goods::where('status',1)->get();
foreach ($goods as $item){
$goods_id=$this->prefix($item);
if (! empty(Redis::llen($goods_id))) {
Log::info($goods_id.'已经设置了库存了');
break;
}else{
for ($i = 1; $i <= $item->stock; $i++) {
// lpush从链表头部添加元素
Redis::lpush($goods_id, $i);
}
// 设置过期时间
$this->setTime($item);
Log::info($goods_id.'商品存入队列成功,数量:'.Redis::llen($goods_id).' 生存时间为:'.Redis::ttl($goods_id));
}
}
}
/**
* @description 设置生存时间 秒杀活动时间
* @param Goods $item
*/
public function setTime(Goods $item)
{
$goods_id=$this->prefix($item);
$seconds=Carbon::parse($item->expired_at)->diffInSeconds(now(), true);
// 设置 goods_name 过期时间,相当于活动时间
Redis::expire($goods_id, $seconds);
}
/**
* @description 抢购
*/
public function start()
{
$id=1;
$goods=Goods::where('status',1)->find($id);
$uid = mt_rand(1,50);//uniqid('user_id', false); // 假设用户ID
// 获取成功抢购结果
$result = Redis::lrange($this->goods_user_success($goods), 0, 100);
// 如果有一个用户只能抢一次,可以加上下面判断
if (in_array($uid, $result)) {
echo '你已经抢过了';
Log::info($uid.'你已经抢过了');
exit;
}
// 将用户加入队列中
Redis::lpush($this->goods_user($goods), $uid);
// 设置并发条件 防止队列数据过大,redis服务器炸掉 如果还在排队抢购的人数超过当前库存数提示库存不足
$user_queue=Redis::llen($this->goods_user($goods))-Redis::llen($this->goods_user_success($goods));
if ($user_queue > Redis::llen($this->prefix($goods))) {
echo '被抢完了';
Log::info($uid.'被抢完了');
exit;
}
if(empty(Redis::llen($this->prefix($goods)))){
echo '库存不足';
Log::info($uid.'库存不足');
}else{
// 从链表的头部删除一个元素,返回删除的元素,因为pop操作是原子性,即使很多用户同时到达,也是依次执行
$count = Redis::lpop($this->prefix($goods));
if (! $count) {
echo '被抢完了';
Log::info($uid.'被抢完了');
exit;
}
$order=new Order();
$order->user_id=$uid;
$order->goods_id=$id;
$order->order_sn='SN_'.date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
$order->save();
Goods::where('id',$id)->decrement('stock',1);
$goods=Goods::where('status',1)->find($id);
Log::info('加订单'.$order->id.' 剩余库存'.$goods->stock);
$msg = '抢到的人为:'.$uid.'抢到第'.$count.'个';
Log::info($msg);
Redis::lpush($this->goods_user_success($goods), $uid);
}
}
/**
* @description 查看抢购用户 抢到结果
*/
public function result()
{
$id=1;
$goods=Goods::where('status',1)->find($id);
$goods_user = Redis::lrange($this->goods_user($goods), 0, 100);
$goods_user_success = Redis::lrange($this->goods_user_success($goods), 0, 100);
dd($goods_user_success,$goods_user);
}
}
猜你喜欢
微信商户向用户打款
阅读 814商户打款到用户零钱
Laravel验证码
阅读 528Composer生成Laravel验证码
swoole 极简聊天室
阅读 571五分钟教你写超简单的swoole聊天室
第一个go网站
阅读 392使用go gin 搭建一个网站
Swoole 扩展安装与使用入门
阅读 475Swoole从入门到实战
在 Laravel 中集成 Swoole 实现 WebSocket 服务器
阅读 1046基于 LaravelS 扩展包把 Swoole 集成到 Laravel 项目来实现 WebSocket 服务器,以便与客户端进行 WebSocket 通信从而实现广播功能。
基于 Swoole 实现简单的 WebSocket 服务器及客户端
阅读 507基于 Swoole 实现简单的 WebSocket 服务器及客户端
PHP定时任务
阅读 514PHP框架Laravel定时任务的实现