Redis + ThinkPHP 实战学习手册(含秒杀场景)

Redis + ThinkPHP 实战学习手册(含秒杀场景)

目录

  1. 基础准备:ThinkPHP 集成 Redis

  2. Redis 核心数据结构(ThinkPHP 用法)

  3. 秒杀场景核心:Redis 原子性与事务

  4. ThinkPHP + Redis 实战场景(秒杀 / 缓存 / 限流)

  5. 常见问题与面试避坑


一、基础准备:ThinkPHP 集成 Redis

1.1 环境要求

  • ThinkPHP 5.1+/6.0+(推荐 6.0+,缓存扩展更完善)

  • PHP Redis 扩展(php_redis.dll,需在 php.ini 中启用)

  • Redis 服务(本地 / 服务器部署,默认端口 6379)

1.2 配置 Redis(ThinkPHP)

步骤 1:修改配置文件

config/cache.php 中配置 Redis 缓存驱动:

dart 复制代码
return [

   // 默认缓存驱动

   'default' => env('cache.driver', 'redis'),

   // 缓存连接配置

   'stores'  => [

       'redis' => [

           'type'       => 'redis',

           'host'       => env('redis.host', '127.0.0.1'),

           'port'       => env('redis.port', 6379),

           'password'   => env('redis.password', ''), // 无密码留空

           'select'     => env('redis.select', 0), // 数据库索引(0-15)

           'timeout'    => 0, // 超时时间

           'persistent' => false, // 是否长连接

           'prefix'     => 'tp_seckill_', // 缓存前缀(避免键名冲突)

       ],

   ],

];
步骤 2:环境变量配置(.env 文件)

在项目根目录 .env 中添加 Redis 配置(可选,优先级更高):

ini 复制代码
REDIS_HOST=127.0.0.1

REDIS_PORT=6379

REDIS_PASSWORD=

REDIS_SELECT=0
步骤 3:测试连接

在 ThinkPHP 控制器中测试 Redis 是否可用:

php 复制代码
namespace app\controller;

use think\facade\Cache;

class RedisTest

{

   public function test()

   {

       // 写入缓存

       Cache::set('test_key', 'hello redis', 3600); // 有效期1小时

       // 读取缓存

       $value = Cache::get('test_key');

       echo $value; // 输出:hello redis

      

       // 直接操作 Redis 原生方法(获取 Redis 句柄)

       $redis = Cache::store('redis')->handler();

       $redis->set('native_key', '原生方法测试');

       echo $redis->get('native_key'); // 输出:原生方法测试

   }

}

二、Redis 核心数据结构(ThinkPHP 用法)

Redis 5 种核心数据结构,对应 ThinkPHP 缓存操作,重点掌握秒杀常用的 String Hash List

2.1 String(字符串)- 秒杀库存存储

用途:存储商品库存、用户 token、计数器等
ThinkPHP 操作示例:
php 复制代码
// 1. 设置值(库存初始化:商品ID=1001,库存=100)

Cache::set('seckill_stock_1001', 100, 86400);

// 2. 读取值

$stock = Cache::get('seckill_stock_1001'); // 100

// 3. 原子自减(核心:秒杀扣库存,天然原子性)

$remainStock = Cache::decr('seckill_stock_1001'); // 99(返回减后的值)

// 原子自增(比如统计秒杀参与人数)

Cache::incr('seckill_count_1001'); // 1

// 4. 设置过期时间(单独设置)

Cache::expire('seckill_stock_1001', 3600); // 1小时后过期

// 5. 原生方法(比如批量设置)

$redis = Cache::store('redis')->handler();

$redis->mset([

   'seckill_stock_1002' => 200,

   'seckill_stock_1003' => 150

]);

2.2 Hash(哈希)- 存储商品详情、用户信息

用途:存储结构化数据(比如商品信息,避免多个 String 键)
ThinkPHP 操作示例:
css 复制代码
// 1. 存储商品信息(商品ID=1001)

Cache::hSet('seckill_goods_1001', 'name', 'iPhone 14');

Cache::hSet('seckill_goods_1001', 'price', 5999);

Cache::hSet('seckill_goods_1001', 'stock', 100);

// 2. 读取单个字段

$price = Cache::hGet('seckill_goods_1001', 'price'); // 5999

// 3. 读取所有字段

$goodsInfo = Cache::hGetAll('seckill_goods_1001');

// 输出:['name' => 'iPhone 14', 'price' => 5999, 'stock' => 100]

// 4. 原子自减(直接操作哈希中的库存字段)

Cache::hDecr('seckill_goods_1001', 'stock'); // 库存99

2.3 List(列表)- 异步队列、秒杀订单排队

用途:实现异步队列(比如秒杀成功后,异步同步订单到 MySQL)
ThinkPHP 操作示例:
php 复制代码
// 1. 入队(秒杀成功后,将订单信息加入队列)

$orderInfo = [

   'order_id' => uniqid(),

   'goods_id' => 1001,

   'user_id' => 10086,

   'create_time' => time()

];

Cache::lpush('seckill_order_queue', json_encode($orderInfo)); // 左入队

// 2. 出队(消费队列:同步订单到 MySQL)

$orderJson = Cache::rpop('seckill_order_queue'); // 右出队(FIFO队列)

$order = json_decode($orderJson, true);

// 执行 MySQL 插入订单逻辑...

// 3. 查看队列长度

$queueLen = Cache::llen('seckill_order_queue'); // 队列中的订单数

2.4 Set(集合)- 去重、抽奖

用途:防止重复秒杀(存储已秒杀用户 ID,天然去重)
ThinkPHP 操作示例:
php 复制代码
// 1. 添加用户到已秒杀集合(用户ID=10086,商品ID=1001)

$isAdd = Cache::sAdd('seckill_user_1001', 10086);

// 返回1:添加成功(用户未秒杀过);返回0:添加失败(用户已秒杀)

// 2. 判断用户是否已秒杀

$hasSeckill = Cache::sIsMember('seckill_user_1001', 10086); // true/false

// 3. 获取已秒杀用户总数

$userCount = Cache::sCard('seckill_user_1001'); // 1

// 4. 移除用户(退款场景)

Cache::sRem('seckill_user_1001', 10086);

2.5 ZSet(有序集合)- 排行榜

用途:秒杀销量排行榜、积分排名
ThinkPHP 操作示例:
php 复制代码
// 1. 添加商品销量到有序集合(商品ID=1001,销量=50)

Cache::zAdd('seckill_sales_rank', 50, 1001);

Cache::zAdd('seckill_sales_rank', 30, 1002);

// 2. 按销量降序排列(取前10名)

$rank = Cache::zRevRange('seckill_sales_rank', 0, 9, true);

// 输出:[1001 => 50, 1002 => 30](键=商品ID,值=销量)

// 3. 增加商品销量(原子操作)

Cache::zIncrBy('seckill_sales_rank', 1, 1001); // 商品1001销量变为51

三、秒杀场景核心:Redis 原子性与事务

3.1 为什么秒杀必须保证原子性?

  • 秒杀核心痛点:高并发下超卖、库存不一致

  • 反例(非原子操作,会超卖):

php 复制代码
// 错误代码:先查库存,再扣减(两步非原子,高并发下超卖)

$stock = Cache::get('seckill_stock_1001');

if ($stock > 0) {

   Cache::set('seckill_stock_1001', $stock - 1); // 高并发下多个请求同时执行,导致库存为负

}
  • 核心解决方案:用 Redis 原子命令或 Lua 脚本,将 "查库存 + 扣库存" 封装为不可分割的操作

3.2 Redis 原子性实现方式(ThinkPHP 实战)

方式 1:使用 Redis 原子命令(推荐简单场景)

Redis 单个命令天然原子性,比如 decr hDecr,直接用于扣库存:

php 复制代码
// 秒杀扣库存核心代码(原子操作,无超卖)

public function seckill($goodsId, $userId)

{

   $stockKey = "seckill_stock_{$goodsId}";

   $userKey = "seckill_user_{$goodsId}";

  

   // 1. 先判断用户是否已秒杀(Set去重)

   if (Cache::sIsMember($userKey, $userId)) {

       return ['code' => 0, 'msg' => '已参与秒杀,请勿重复提交'];

   }

  

   // 2. 原子扣减库存(decr返回减后的值,库存不足时返回-1)

   $remainStock = Cache::decr($stockKey);

   if ($remainStock  {

       return ['code' => 0, 'msg' => '库存不足'];

   }

  

   // 3. 扣减成功,添加用户到已秒杀集合

   Cache::sAdd($userKey, $userId);

  

   // 4. 加入异步队列,同步订单到MySQL

   $orderInfo = [/* 订单数据 */];

   Cache::lpush('seckill_order_queue', json_encode($orderInfo));

  

   return ['code' => 1, 'msg' => '秒杀成功'];

}
方式 2:Lua 脚本(复杂逻辑原子性,推荐秒杀场景)

当需要 "判断库存> 0 + 扣库存 + 记录用户" 多步逻辑时,用 Lua 脚本保证原子性:

php 复制代码
// ThinkPHP 中调用 Lua 脚本扣库存

public function seckillByLua($goodsId, $userId)

{

   $redis = Cache::store('redis')->handler();

   $stockKey = "seckill_stock_{$goodsId}";

   $userKey = "seckill_user_{$goodsId}";

  

   // Lua 脚本:判断库存+扣库存+记录用户(原子操作)

   $lua = << stockKey = KEYS[1]

   local userKey = KEYS[2]

   local userId = ARGV[1]

  

   -- 1. 判断用户是否已秒杀

   if redis.call('sismember', userKey, userId) == 1 then

       return 0 -- 已秒杀

   end

  

   -- 2. 判断库存是否充足

   local stock = tonumber(redis.call('get', stockKey))

   if not stock or stock

       return -1 -- 库存不足

   end

  

   -- 3. 扣减库存 + 记录用户

   redis.call('decr', stockKey)

   redis.call('sadd', userKey, userId)

   return 1 -- 秒杀成功

   LUA;

  

   // 执行 Lua 脚本(KEYS参数:2个键;ARGV参数:用户ID)

   $result = $redis->eval($lua, [$stockKey, $userKey, $userId], 2);

  

   switch ($result) {

       case 1:

           // 加入订单队列...

           return ['code' => 1, 'msg' => '秒杀成功'];

       case 0:

           return ['code' => 0, 'msg' => '已参与秒杀'];

       case -1:

           return ['code' => 0, 'msg' => '库存不足'];

   }

}

3.3 Redis 事务(MULTI/EXEC)- 了解即可

Redis 事务不支持回滚,适合无逻辑依赖的批量操作,秒杀场景用得少:

scss 复制代码
$redis = Cache::store('redis')->handler();

$redis->multi(); // 开启事务

$redis->decr('seckill_stock_1001');

$redis->sAdd('seckill_user_1001', 10086);

$redis->exec(); // 提交事务(批量执行,原子性)

3.4 Redis 三大特性在秒杀中的体现

特性 说明(ThinkPHP 秒杀场景)
原子性 decr 或 Lua 脚本,保证 "扣库存 + 记录用户" 不可分割,避免超卖
一致性 库存从 Redis 预扣减,再异步同步到 MySQL;Redis 拒绝非法操作(比如对字符串库存 decr)
隔离性 Redis 单线程执行命令,秒杀高并发请求按顺序执行,不会出现 "同时读库存、同时扣减" 的并发问题

注意:Redis 不保证持久性(秒杀场景可接受,因为 MySQL 是最终数据源,Redis 宕机可从 MySQL 恢复库存)


四、ThinkPHP + Redis 实战场景扩展

4.1 缓存穿透解决方案(秒杀场景:恶意查询不存在的商品)

  • 问题:用户频繁查询不存在的商品 ID,导致请求穿透 Redis 直接访问 MySQL

  • 解决方案:缓存空值 + 布隆过滤器(推荐)

php 复制代码
// 缓存空值示例

public function getGoodsInfo($goodsId)

{

   $cacheKey = "goods_info_{$goodsId}";

   $info = Cache::get($cacheKey);

  

   if ($info === false) {

       // 缓存未命中,查询MySQL

       $info = Db::name('goods')->find($goodsId);

       if (!$info) {

           // 存储空值,设置短期过期(比如10分钟)

           Cache::set($cacheKey, json_encode(null), 600);

           return ['code' => 0, 'msg' => '商品不存在'];

       }

       // 缓存商品信息(1小时过期)

       Cache::set($cacheKey, json_encode($info), 3600);

   } else {

       $info = json_decode($info, true);

       if ($info === null) {

           return ['code' => 0, 'msg' => '商品不存在'];

       }

   }

   return ['code' => 1, 'data' => $info];

}

4.2 缓存击穿解决方案(秒杀场景:热点商品缓存过期)

  • 问题:秒杀热点商品的缓存过期瞬间,大量请求直接打 MySQL

  • 解决方案:互斥锁 + 缓存预热

php 复制代码
// 互斥锁示例(ThinkPHP 结合 Redis setnx 实现)

public function getHotGoodsInfo($goodsId)

{

   $cacheKey = "hot_goods_{$goodsId}";

   $lockKey = "lock_hot_goods_{$goodsId}";

  

   $info = Cache::get($cacheKey);

   if ($info) {

       return json_decode($info, true);

   }

  

   // 缓存未命中,获取互斥锁

   $lock = Cache::setnx($lockKey, 1);

   if ($lock) {

       // 获得锁,查询MySQL并更新缓存

       $info = Db::name('goods')->find($goodsId);

       Cache::set($cacheKey, json_encode($info), 3600); // 延长缓存时间

       Cache::del($lockKey); // 释放锁

       return $info;

随便学一下这个东西,理解一下该怎么做就好

相关推荐
代码or搬砖3 小时前
Spring Cache讲解
java·后端·spring
Json_4 小时前
springboot框架 线程池使用与配置,简单粗暴直接用,再也不用自己创建线程了~
java·spring boot·后端
sin604 小时前
学习笔记:Mybatis 示例代码,应用场景,面试题
后端
前端小张同学4 小时前
餐饮小程序需要你们
java·前端·后端
王中阳Go4 小时前
都2026年了,PHP还纠结转Go还是Java呢?安利一个无缝迁移的框架~
java·后端·go
老华带你飞4 小时前
二手商城|基于springboot 二手商城系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
Tadas-Gao4 小时前
GraphQL:下一代API架构的设计哲学与实践创新
java·分布式·后端·微服务·架构·graphql
老华带你飞5 小时前
酒店预约|基于springboot 酒店预约系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
dob5 小时前
为什么你的if-else越写越乱?聊聊状态机
后端