redis秒杀实现

一、核心功能实现

1. 秒杀前:库存缓存预热

秒杀开始前,将商品库存加载到 Redis,避免秒杀时直接查询数据库。

可通过命令行或定时任务执行预热(推荐秒杀开始前 10 分钟执行)。

// 将库存写入Redis(原子操作,防止重复预热)

php 复制代码
Redis::setnx('seckill:stock:'.$goodId, $goods->stock)

2. 核心:秒杀逻辑实现(防超卖 + 防重复下单 + 限流)

php 复制代码
namespace App\Services;

use App\Models\SeckillGoods;
use App\Models\SeckillOrder;
use Carbon\Carbon;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

class SeckillService
{
    // ... 原有代码

    /**
     * 秒杀核心逻辑
     * @param int $userId 用户ID
     * @param int $goodsId 商品ID
     * @return array 结果数组
     */
    public function seckill(int $userId, int $goodsId): array
    {
       //此次省略一些其他基本登录校验
       //1.基础校验(秒杀时间、商品状态)
        $goods = SeckillGoods::find($goodsId);
        if (!$goods) {
            return ['code' => 400, 'msg' => '商品不存在'];
        }
        if ($goods->status != 1) {
            return ['code' => 400, 'msg' => '秒杀未开始或已结束'];
        }
        if (Carbon::now()->lt($goods->start_time) || Carbon::now()->gt($goods->end_time)) {
            return ['code' => 400, 'msg' => '当前非秒杀时段'];
        }

        // 2. 用户限流(1分钟内最多5次请求,防止恶意刷请求)
        $requestCount = Redis::incr('seckill:limit:user_'.$userId);
        if ($requestCount == 1) {
            Redis::expire($limitKey, 60); // 设置1分钟过期
        }
        if ($requestCount > 5) {
            return ['code' => 400, 'msg' => '请求过于频繁,请稍后再试'];
        }

        // 3. 防重复下单(同一用户只能秒杀同一商品1次)
        $orderKey = 'seckill:order:user_' . $userId . '_' . $goodsId;
        if (Redis::exists($orderKey)) {
            return ['code' => 400, 'msg' => '你已参与该商品秒杀,请勿重复下单'];
        }

        // 4. Redis原子扣减库存(核心:防止超卖,decr是原子操作)
        $stockKey = 'seckill:stock:' . $goodsId;
        $leftStock = Redis::decr($stockKey);

        // 库存不足处理
        if ($leftStock < 0) {
            Redis::incr($stockKey); // 回滚库存,避免负数
            return ['code' => 400, 'msg' => '库存不足,秒杀失败'];
        }

        // 5. 标记用户已下单(设置过期时间,与秒杀结束时间一致)
        $expireSeconds = $goods->end_time->diffInSeconds(Carbon::now());
        Redis::setex($orderKey, $expireSeconds, 1);

        // 6. 异步创建订单(放入消息队列,提升响应速度)
        $this->createOrderAsync($userId, $goodsId, $goods->price);

        return ['code' => 200, 'msg' => '秒杀成功,正在为你创建订单'];
    }

    /**
     * 异步创建订单
     * @param int $userId
     * @param int $goodsId
     * @param float $price
     */
    protected function createOrderAsync(int $userId, int $goodsId, float $price): void
    {
        // 分发任务到队列(需先创建订单任务类)
        \App\Jobs\CreateSeckillOrderJob::dispatch($userId, $goodsId, $price)
            ->onQueue('seckill'); // 指定秒杀队列,优先处理
    }

    /**
     * 生成唯一订单号
     * @return string
     */
    protected function generateOrderNo(): string
    {
        return date('YmdHis') . Str::random(6) . mt_rand(1000, 9999);
    }
}

3. 异步订单任务(解决页面卡顿)

创建订单任务类,异步处理订单入库与数据库库存扣减:

php 复制代码
namespace App\Jobs;

use App\Models\SeckillGoods;
use App\Models\SeckillOrder;
use App\Services\SeckillService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;

class CreateSeckillOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $userId;
    protected $goodsId;
    protected $price;

    /**
     * 任务超时时间
     * @var int
     */
    public $timeout = 60;

    public function __construct(int $userId, int $goodsId, float $price)
    {
        $this->userId = $userId;
        $this->goodsId = $goodsId;
        $this->price = $price;
    }

    /**
     * 执行任务
     * @return void
     */
    public function handle()
    {
        // 1. 数据库原子扣减库存(兜底校验,防止Redis与数据库不一致)
        $service = new SeckillService();
        $goods = SeckillGoods::find($this->goodsId);
        if (!$goods) {
            // 库存回滚
            Redis::incr('seckill:stock:' . $this->goodsId);
            return;
        }

        DB::beginTransaction();
        try {
            // 原子更新:只有库存>0时才扣减
            $affectedRows = SeckillGoods::where('id', $this->goodsId)
                ->where('stock', '>', 0)
                ->update([
                    'stock' => DB::raw('stock - 1')
                ]);

            if ($affectedRows == 0) {
                throw new \Exception('数据库库存不足');
            }

            // 2. 创建订单
            $orderNo = $service->generateOrderNo();
            SeckillOrder::create([
                'order_no' => $orderNo,
                'user_id' => $this->userId,
                'goods_id' => $this->goodsId,
                'amount' => $this->price,
                'pay_status' => 0,
                'status' => 0
            ]);

            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            // 异常处理:Redis库存回滚,删除用户下单标记
            Redis::incr('seckill:stock:' . $this->goodsId);
            Redis::del('seckill:order:user_' . $this->userId . '_' . $this->goodsId);
            \Log::error("创建秒杀订单失败:" . $e->getMessage());
        }
    }
}

4. 控制器实现(提供接口---前端异步请求获取库存接口)

php 复制代码
namespace App\Http\Controllers;

use App\Services\SeckillService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class SeckillController extends Controller
{
    /**
     * 秒杀接口
     * @param Request $request
     * @return \Illuminate\Http\JsonResponse
     */
    public function submit(Request $request)
    {
        // 验证用户登录(根据实际业务调整)
        if (!Auth::check()) {
            return response()->json(['code' => 401, 'msg' => '请先登录']);
        }
        $userId = Auth::id();
        $goodsId = $request->input('goods_id');

        // 参数验证
        if (!$goodsId) {
            return response()->json(['code' => 400, 'msg' => '商品ID不能为空']);
        }

        // 执行秒杀
        $service = new SeckillService();
        $result = $service->seckill($userId, (int)$goodsId);

        return response()->json($result);
    }

 
   /**
     * 获取商品库存(供前端异步查询 + 添加互斥锁,防止缓存击穿)
     * @param int $goodsId
     * @return JsonResponse
     */
    public function getStock(int $goodsId): JsonResponse
    {
        // 1. 定义Redis键(库存键 + 互斥锁键)
        $stockKey = 'seckill:stock:' . $goodsId;
        $lockKey = "seckill:lock:stock:{$goodsId}"; // 互斥锁的Key(专门用于缓存击穿防护)
        $lockExpire = 5; // 互斥锁过期时间5秒(防止死锁,需大于数据库查询耗时)

        // 2. 先尝试获取Redis库存缓存
        $stock = Redis::get($stockKey);
        if ($stock !== false) {
            // 缓存存在,直接返回(无需加锁)
            return response()->json([
                'code' => 200,
                'msg' => 'success',
                'data' => ['stock' => (int)$stock]
            ]);
        }

        // 3. 缓存不存在(可能失效/库存为0),添加互斥锁,防止大量请求穿透到数据库
        try {
            // 核心:使用SETNX实现互斥锁(原子操作,只有一个请求能获取到锁)
            $isLockSuccess = Redis::setnx($lockKey, 1);
            if (!$isLockSuccess) {
                // 未获取到锁:其他请求正在查询数据库,短暂休眠后重试(或直接返回0,根据业务调整)
                usleep(100000); // 休眠100毫秒,避免频繁请求
                return $this->getStock($goodsId); // 递归重试,获取缓存数据
            }

            // 4. 获取到锁:唯一允许穿透到数据库查询的请求
            Redis::expire($lockKey, $lockExpire); // 设置锁过期时间,防止死锁

            // 从数据库查询真实库存
            $goods = SeckillGoods::find($goodsId);
            $realStock = $goods ? (int)$goods->stock : 0;

            // 5. 将数据库查询结果重新写入Redis缓存(设置过期时间,避免再次立即失效)
            // 针对缓存雪崩:添加随机过期时间(1800±300秒),避免所有商品库存缓存同时失效
            $expireTime = 1800 + mt_rand(-300, 300); 
            Redis::setex($stockKey, $expireTime, $realStock);

            // 6. 返回真实库存
            return response()->json([
                'code' => 200,
                'msg' => 'success',
                'data' => ['stock' => $realStock]
            ]);
        } finally {
            // 7. 释放互斥锁(无论是否异常,都要释放锁,避免死锁)
            Redis::del($lockKey);
        }
    }
   
}
互斥锁的运行流程(直观理解缓存击穿防护)

假设 1000 个请求同时查询「商品 ID=1」的库存,此时 Redis 库存缓存恰好失效:

  1. 第 1 个请求:查询 Redis 缓存不存在 → 调用setnx获取锁成功 → 穿透到数据库查询真实库存 → 写入 Redis 缓存 → 释放锁 → 返回结果;
  2. 第 2-1000 个请求:查询 Redis 缓存不存在 → 调用setnx获取锁失败 → 休眠 100 毫秒后递归重试 → 此时第 1 个请求已将库存写入 Redis → 重试后直接获取 Redis 缓存 → 返回结果,无需穿透数据库;
  3. 最终效果:只有 1 个请求穿透到数据库,其余 999 个请求均从 Redis 获取数据,成功防止缓存击穿。

5、总结

  1. 互斥锁的具体体现:在上述getStock(获取库存)方法中,通过seckill:lock:stock:{$goodsId}作为锁 Key,使用**setnx实现原子锁,**确保缓存失效时只有一个请求穿透到数据库;
  2. 互斥锁的核心流程:「查询缓存→缓存不存在→获取锁→查库写缓存→释放锁→返回结果」,未获取锁的请求休眠后重试,复用缓存数据;
  3. 缓存雪崩防护:在写入库存缓存时设置「随机过期时间」,搭配 Redis 集群,避免缓存集中失效和服务单点故障。

二、关键优化与注意事项

1. 启动队列消费

异步订单依赖队列,需启动队列消费进程:

php 复制代码
# 普通启动
php artisan queue:work --queue=seckill,default

# 守护进程启动(生产环境推荐)
php artisan queue:work --queue=seckill,default --daemon --timeout=60 --tries=3

2. 防止缓存击穿 / 雪崩

  • 缓存击穿 :给库存 Key 添加互斥锁,当库存为 0 时,防止大量请求穿透到数据库(可在getStock方法中添加锁逻辑)。
  • 缓存雪崩:给 Redis 库存 Key 设置随机过期时间(避免所有商品库存同时失效),或使用 Redis 集群提高可用性。

3. 库存一致性保障

  • 秒杀结束后,通过定时任务对比 Redis 库存与数据库库存,修正差异

4. Laravel 性能优化

  • 开启 OPcache(php.ini中配置opcache.enable=1),提升 PHP 代码执行效率。
  • 优化 PHP-FPM 配置(根据服务器 CPU 核心数调整pm.max_children等参数)。
  • 使用 Redis 集群或哨兵模式,提高 Redis 可用性。

三、核心原理总结

  1. 防超卖 :通过 Redis的decr原子操作扣减库存,配合数据库原子更新兜底,确保库存不会为负。
  2. 防重复下单:通过 Redis 键记录用户已下单状态,避免同一用户重复秒杀。
  3. 防请求穿透:前端限流 + 后端用户限流,减少无效请求;缓存预热减轻数据库压力。
  4. 解决页面卡顿:将订单创建等耗时操作异步化,通过消息队列削峰,快速返回秒杀结果。
相关推荐
BingoGo6 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack6 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack1 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo2 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack3 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
QQ5110082854 天前
python+springboot+django/flask的校园资料分享系统
spring boot·python·django·flask·node.js·php
WeiXin_DZbishe4 天前
基于django在线音乐数据采集的设计与实现-计算机毕设 附源码 22647
javascript·spring boot·mysql·django·node.js·php·html5