记一次php laravel redis 简易的秒杀活动

本次使用docker容器
1.秒杀的商品同步到redis数据库中

执行命令同步商品信息

docker exec -it laravel-app php artisan app:init-seckill-stock

<?php

namespace App\Console\Commands;

use App\Models\admin\Storage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use App\Services\SeckillService;

class InitSeckillStock extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:init-seckill-stock';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle(SeckillService $seckill)
    {
        $this->info("Initializing seckill stock into Redis...");

        $products = Storage::where('is_usable', '>', 0)->get();

        foreach ($products as $product) {
            $seckill->initStock($product->product_id, $product->sku_id, $product->qty, 360000);
            $this->info("Stock set for product {$product->product_id} - SKU {$product->sku_id}");
        }

        $this->info("Seckill stock initialization complete.");

    }
}

 2.模拟秒杀活动

/**
     *
     * 执行秒杀活动
     *
     * */
     public function testKill(SeckillService $seckill){
        $productID=36763;
        $sku_id=42590;
        for($i=0;$i<=1000;$i++){
            $userID=rand(1,999);
            $result=$this->seckill($productID,$sku_id,$userID,$seckill);
            if ($result === -1) {
                echo "库存为 0,秒杀活动提前结束。\n";
                break; // 立即打断循环
            }
        }
        print_r('秒杀活动结束');
    }
    public function seckill( $productId,$skuId,$userId,SeckillService $seckill)
    {
        $kc=$seckill->getStock($productId,$skuId);
        if($kc){
            // 判断是否已秒杀(防止重复参与)
            if (Redis::sismember("seckill:user:".$productId.":".$skuId, $userId)) {
                Log::info("重复秒杀尝试", [
                    'product_id' => $productId,
                    'sku_id' => $skuId,
                    'user_id' => $userId,
                    'time' => now()->toDateTimeString()
                ]);
                return false;
            }

            // 执行秒杀
            if ($seckill->attemptSeckill($productId,$skuId)) {
                // 记录用户已秒杀
                Redis::sadd("seckill:user:".$productId.":".$skuId, $userId);

                // 推入订单队列(简化示例)
                Redis::rpush("seckill:queue:".$productId.":".$skuId, json_encode([
                    'product_id' => $productId,
                    'sku_id' => $skuId,
                    'user_id' => $userId,
                    'time' => time(),
                ]));
                return response()->json(['msg' => '秒杀成功,正在生成订单']);
            } else {
                return response()->json(['msg' => '秒杀失败,库存不足'], 410);
            }
        }else{
            return -1;
        }
    }

3.进行redis 库存扣减

<?php

namespace App\Services;

use Illuminate\Support\Facades\Redis;

class SeckillService
{
    /**
     * 秒杀库存扣减
     *
     * @param string $skuId SKU 编号
     * @param string $skuId SKU 编号
     * @return bool true 表示扣减成功,false 表示库存不足或出错
     */
    public function attemptSeckill(string $productId,string $skuId): bool
    {
        $key = "seckill:stock:".$productId.":".$skuId;

        // Lua 脚本:原子扣减库存
        $lua = <<<LUA
            local stock = tonumber(redis.call("get", KEYS[1]))
            if not stock or stock <= 0 then
                return 0
            end
            redis.call("decr", KEYS[1])
            return 1
            LUA;
        $result = Redis::eval($lua, 1, $key);
        return $result == 1;
    }

    /**
     * 初始化秒杀库存
     *
     * @param string $productId SKU 编号
     * @param string $skuId SKU 编号
     * @param int $qty 库存数量
     * @param int|null $ttl 过期时间(秒),可选
     */
    public function initStock(string $productId,string $skuId, int $qty, ?int $ttl = null): void
    {
        $key = "seckill:stock:".$productId.":".$skuId;

        Redis::set($key, $qty);
        if ($ttl) {
            Redis::expire($key, $ttl);
        }
    }

    /**
     * 获取当前库存
     *
     * @param string $skuId
     * @return int|null
     */
    public function getStock(string $productId,string $skuId): ?int
    {
        $key = "seckill:stock:".$productId.":".$skuId;
        $qty = Redis::get($key);
        return $qty !== null ? (int) $qty : null;
    }
}

 4.把订单信息推入到redis队列

手动开启执行命令

docker exec -it laravel-app php artisan seckill:consume

<?php

namespace App\Console\Commands;

use App\Jobs\ProcessSeckillOrder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class ConsumeSeckillQueue extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'seckill:consume';


    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '从 Redis 队列中读取秒杀数据并派发队列 Job';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $queueKey = 'seckill:queue:36763:42590';
        $processingKey = 'seckill:queue:processing';
        Log::info("秒杀队列消费者启动,监听队列:$queueKey");
        while (true) {
            $data = Redis::rpoplpush($queueKey,$processingKey);

            if ($data) {
                $payload = json_decode($data, true);
                try {
                    if (is_array($payload)) {
                        Log::info('派发秒杀任务', $payload);
                        ProcessSeckillOrder::dispatch($payload['user_id'], $payload['product_id'],$payload['sku_id']);
                    }else {
                        Log::warning("Redis 中的数据无法解析:$data");
                        // 移除坏数据
                        Redis::lrem($processingKey, 0, $data);
                    }
                }catch (\Exception $e) {
                    Log::error("消费者异常中断:" . $e->getMessage());
                    sleep(1);
                }
            } else {
                usleep(50000); // 空队列,休眠50ms
            }
        }
    }
}

 
5. 后台任务消费队列 Job

手动执行命令

docker exec -it laravel-app php artisan queue:work

<?php

namespace App\Jobs;

use App\Models\admin\SeckillOrders;
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;
use Illuminate\Support\Facades\Log;
//use Psy\Util\Str;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Str;

class ProcessSeckillOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
    protected $userId;
    protected $productId;
    protected $skuId;
    /**
     * Create a new job instance.
     */
    public function __construct($userId, $productId,$skuId)
    {
        $this->userId = $userId;
        $this->productId = $productId;
        $this->skuId = $skuId;
    }

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        $processingKey = 'seckill:queue:processing';

        $data = json_encode([
            'user_id'    => $this->userId,
            'product_id' => $this->productId,
            'sku_id'     => $this->skuId,
        ]);
        DB::beginTransaction();

        try {
            // 防止重复写入
            $exists = SeckillOrders::where('user_id', $this->userId)
                ->where('product_id', $this->productId)
                ->where('sku_id', $this->skuId)
                ->exists();

            if (!$exists) {
                SeckillOrders::create([
                    'user_id'    => $this->userId,
                    'product_id' => $this->productId,
                    'sku_id' => $this->skuId,
                    'qty'   => 1,
                    'order_no'   => Str::uuid()->toString(),
                ]);
                // ✅ 数据写入成功,再从 processing 队列移除任务
                Redis::lrem($processingKey, 0, $data);
            }
            Log::info('写入数据库成功');
            DB::commit();
        } catch (\Exception $e) {
            DB::rollBack();
            Log::error("秒杀订单写入失败:".$e->getMessage());
        }
    }
}

 

posted @ 2025-06-05 15:45  SHACK元  阅读(42)  评论(0)    收藏  举报