上一篇文章我们介绍了swoole搭建HTTP服务端,
这篇文章结合上篇文章中的内容,结合Redis列表打造高并发的秒杀系统,
秒杀系统的重点主要在于: 保证大量用户访问的同时,系统正常顺畅运行,商品库存不超卖
再开始之前,我们先将所需的Redis
及swoole
安装完成,介绍秒杀具体操作流程:
1. 将秒杀商品的基本信息,使用hash
存入Redis
中
2. 用户点击抢购时,将用户ID存入Redis
列表中,同时根据库存,限制列表长度
3. 使用swoole
新建协程,根据列表先进先出,异步实现商品抢购
4. 使用定时器,定时向Redis
中查询抢购结果,设置超时时间,返回抢购结果
这里redis
我们使用了两个hash
、一个list
来存储相关信息:
// 当前商品的库存等信息(hash,例:swoole:goods_10)
protected $goods_key;
// 当前商品队列的用户情况(list,例:swoole:goods_10_user)
protected $user_queue_key;
// 抢购成功的用户订单(hash,例:swoole:goods_10_order)
protected $order_queue_key;
public function goods()
{
$this->setGoods([
'goods_id' => 10, // 商品ID
// 'price' => $data['price'], // 价格
'prom_id' => 10, // 活动ID
'buy_limit' => 1, // 每人限购
'store_count' => 99, // 库存
'sell_count' => 0, // 销量
'start_time' => time(), // 开始时间
'end_time' => strtotime("+1 day"), // 截止时间
'status' => 0, //用来判断协程运行状态
]);
return $this->getGoods(10);
}
开启swoole
服务,访问 http://127.0.0.1:9501/Flashsale/goods?id=1
,保存商品信息
<?php
/**
* @Author: [FENG] <1161634940@qq.com>
* @Date: 2024-08-20 15:09:00
* @Last Modified by: [FENG] <1161634940@qq.com>
* @Last Modified time: 2024-09-01 16:10:10
*/
namespace feng;
/**
* 秒杀系统
*/
class Flashsale
{
// 键名
protected $key;
// redis实例化
protected $redis;
protected $server;
protected $frame;
// swoole Common类实例
protected $swooleCommon;
// 当前商品的库存等信息
protected $goods_key;
// 当前商品队列的用户情况
protected $user_queue_key;
// 抢购成功的用户
protected $order_queue_key;
protected $field = ['prom_id', 'price', 'buy_limit', 'store_count', 'sell_count', 'start_time', 'end_time'];
public function __construct($base = [])
{
if ($base) {
[$this->server, $this->frame, $this->swooleCommon] = $base;
}
$config = [
"hostname" => '127.0.0.1', //Redis主机IP地址
"hostport" => 6379, //Redis端口
"password" => '', //Redis密码
"select" => 6, //Redis数据库索引
"timeout" => 0, //Redis连接超时时间
];
$this->redis = $this->redisInit($config); // 实例化redis
$this->key = 'swoole:'; // 个人习惯redis加上前缀
}
/**
* [goods 存入商品信息]
*/
public function goods()
{
$this->setGoods([
'goods_id' => 10, // 商品ID
// 'price' => $data['price'], // 价格
'prom_id' => 10, // 活动ID
'buy_limit' => 1, // 每人限购
'store_count' => 99, // 库存
'sell_count' => 0, // 销量
'start_time' => time(), // 开始时间
'end_time' => strtotime("+1 day"), // 截止时间
'status' => 0, //用来判断协程运行状态
]);
return $this->getGoods(10);
}
/**
* [FlashSale 抢购详情筛选]
*/
public function flashSale($data = [])
{
$data = $data ?: input('');
if (empty($data['id']))
return result(0, '请选择抢购的商品');
if (empty($data['user_id']))
return result(0, '请登录后进行操作');
$redis = $this->redis;
$goods = $this->getGoods($data['id']);
if (!$goods || time() > $goods['end_time'])
return result(0, '当前商品抢购已结束。');
if (time() < $goods['start_time'])
return result(0, '抢购尚未开始,请耐心等待');
$goods_id = $data['id'];
$user_id = $data['user_id'];
$this->goods_key = $this->key . "goods_".$goods_id;
$this->user_queue_key = $this->key . "goods_".$goods_id."_user";
$this->order_queue_key = $this->key . "goods_".$goods_id."_order";
if ($goods['store_count'] == $goods['sell_count'] || $redis->llen($this->user_queue_key) > $goods['store_count'] * 1.5 ) {
if (time() - $goods['start_time'] > 600) {
// 查询数据库,获取未支付订单数量
$count = Db::name('order')->where(['order_prom_id'=>$goods['prom_id'], 'order_prom_type'=>1, 'pay_status'=>0])->count();
if ($count > 0) {
return result(0, '当前抢购存在未支付的订单,请稍后再次尝试');
}
}
return result(0, '当前商品已被抢完。');
}
if ($redis->exists($this->order_queue_key) && $order_id = $redis->hget($this->order_queue_key, $user_id)) {
return result(0, '你已参与过活动,请勿重复添加');
}
if (!$this->hasExist($this->user_queue_key, $user_id)) {
$redis->rpush($this->user_queue_key, $user_id);
}
$ststus = $redis->rawCommand('hmget', $this->goods_key, 'status');
if ($ststus != 1) {
// 消费者协程,单独执行抢购
go(function () use ($redis, $data, $goods) {
$redis->hset($this->goods_key, 'status', 1);
while($user_id = $redis->lpop($this->user_queue_key)) {
try {
if (!$user_id || $redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count')) {
$redis->hset($this->goods_key, 'status', 0);
die('抢购协程执行结束');
}
$order_id = $this->add_order($goods, $data['user_id']); // 商品加入数据库(添加订单十分钟失效)
if ($order_id) {
$redis->hset($this->order_queue_key, $user_id, $order_id);
$redis->hincrby($this->goods_key, 'sell_count', 1);
}
if ($redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count')) {
$redis->del($this->user_queue_key);
}
if ($redis->llen($this->user_queue_key) == 0) {
$redis->hset($this->goods_key, 'status', 0);
die('抢购协程执行结束');
}
} catch (\Swoole\ExitException $e) {
// global $exit_status;
// $exit_status = $e->getStatus();
}
}
});
}
$data['count'] = 0;
$timer_id = $this->server->tick(200, function($timer_id) use ($redis, &$data, $user_id){
$json = '';
if (!$json && $order_id = $redis->hget($this->order_queue_key, $user_id))
$json = msg(1, '抢购成功.', $order_id);
if (!$json && $redis->hget($this->goods_key, 'store_count') == $redis->hget($this->goods_key, 'sell_count'))
$json = msg(0, '当前商品已被抢光。');
if ($data['count'] > 100)
$json = msg(0, '当前活动过于火爆,请稍后尝试');
if ($json) {
$this->server->clearTimer($timer_id);
$this->frame->end($json);
}
$data['count']++;
});
}
// 生成订单(存入mysql数据库)
public function add_order($goods, $user_id)
{
$order_id = rand(000, 999); // 订单设置15分钟支付超时,同时超时取消订单,释放库存
return $order_id;
}
// 初始化redis
public function redisInit($config)
{
if (!extension_loaded('redis')) {
throw new \Exception('redis扩展未安装');
}
$func = empty($config['persistent']) ? 'connect' : 'pconnect';
$redis = new \Redis;
if (isset($config['socket_type']) && $config['socket_type'] === 'unix') {
$success = $redis->$func($config['socket']);
} else {
$success = $redis->$func($config['hostname'], $config['hostport'], $config['timeout']);
}
if (isset($config['password'])) {
$redis->auth($config['password']);
}
if (isset($config['select']) && !empty($config['select'])) {
$redis->select(intval($config['select']));
}
return $redis;
}
public function setGoods($goods = [])
{
if (is_array($goods) && isset($goods['goods_id'])) {
foreach ($this->field as $v) {
if (isset($goods[$v])) {
$this->redis->hset($this->key . "goods_".$goods['goods_id'], $v, $goods[$v]);
}
}
} else {
$this->redis->del($this->key . "goods_".$goods);
// $this->redis->hdel($goods, 'buy_limit', 'store_count', 'start_time', 'end_time');
}
}
public function getGoods($goods_id, $key = '')
{
if (in_array($key, $this->field)) {
return $this->redis->hget($this->key . "goods_".$goods_id, $key);
} else {
return $this->redis->hgetall($this->key . "goods_".$goods_id);
}
}
public function hasExist($key, $element = '')
{
$redis = $this->redis;
if (!$element) {
// 使用lrange获取整个list
$list = $redis->lrange($key, 0, -1);
return $list;
}
// Lua脚本检查元素是否在list中
$script = <<<LUA
local key = KEYS[1]
local value = ARGV[1]
local element = redis.call('lrange', key, 0, -1)
for i, v in ipairs(element) do
if v == value then
return 1
end
end
return 0
LUA;
$result = $redis->rawCommand("EVAL", $script, 1, $key, $element);
return $result == 1 ? true : false;
}
}
/**
* [result 返回状态数组]
* @param [type] $code [错误码]
* @param string $data [具体数据]
* @return [type] [description]
*/
function result($code, $msg=true, $data=false)
{
if (is_array($code) || $code === true || $msg === true) {
$result = ['code'=>1];
$result['err'] = is_string($msg) ? $msg : '操作成功';
$result['data'] = $code;
} else {
$result = ['code'=>$code, 'err'=>$msg, 'data'=>$data];
}
return $result;
}
访问 url http://127.0.0.1:9501/Flashsale/flashSale?id=10&user_id=1
,执行抢购(这里user_id建议使用token获取)
本文为冯奎原创文章,转载无需和我联系,但请注明来自冯奎博客fengkui.net
最新评论