redis秒杀

redis秒杀

基于redis 实现并发情况的超卖超买解决方案

秒杀功能主要有两个问题要解决: ①高并发对数据库产生的压力 ②竞争状态下如何解决库存的正确减少(“超卖” 问题)。

第一个问题,对于 PHP 来说很简单,用缓存技术就可以缓解数据库压力,比如 memcache,redis 等缓存技术,这里我使用了 redis。 第二个问题,我使用 redis 队列,因为 pop 操作是原子的,即使有很多用户同时到达,也是依次执行。

具体方案

  1. 将商品库存存入redis队列,并按照活动结束时间设置过期时间。
  2. 在用户抢购商品时range抢购成功的用户,判断是否重复抢购
  3. 将用户加入商品抢购队列中
  4. 防止抢购用户过多将redis服务器炸掉,可以设置判断条件,如并发抢购用户不可以大于该商品库存,或者设置最大容许并发几个用户抢购
  5. 判断redis队列中该商品库存是否足够
  6. 使用redis的pop删除一个元素,返回删除的元素,因为pop操作是原子性,即使很多用户同时到达,也是依次执行
  7. 加订单,减库存
  8. 将成功抢购该商品的用户push到成功抢购商品的队列中
  9. 使用ab测压测试抢购

展示file 执行商品队列初始化方法Initialization: file 执行: ab -c 20 -n 200 -T 'application/x-www-form-urlencoded' -p public/post.txt https://blogs.chencong.fun/start(此为post方式ab测压) filefilefilefile

数据库

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);
    }

}
猜你喜欢