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;

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

相关推荐
码事漫谈3 分钟前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈3 分钟前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy30 分钟前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
YDS8291 小时前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq
无限大61 小时前
为什么"区块链"不只是比特币?——从加密货币到分布式应用
后端
洛神么么哒1 小时前
freeswitch-初级-01-日志分割
后端
蝎子莱莱爱打怪1 小时前
我的2025年年终总结
java·后端·面试
奋进的芋圆1 小时前
TokenRetryHelper 详解与 Spring Boot 迁移方案
java·spring boot·后端
云上小朱2 小时前
软件部署-在k8s部署Hadoop集群
后端
镜花水月linyi2 小时前
Cookie、Session、JWT 的区别?
后端·面试