news 2026/1/12 11:30:03

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 缓存驱动:

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 配置(可选,优先级更高):

REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD= REDIS_SELECT=0
步骤 3:测试连接

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

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 缓存操作,重点掌握秒杀常用的StringHashList

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

用途:存储商品库存、用户 token、计数器等
ThinkPHP 操作示例:
// 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 操作示例:
// 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 操作示例:
// 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 操作示例:
// 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 操作示例:
// 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 为什么秒杀必须保证原子性?

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

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

// 错误代码:先查库存,再扣减(两步非原子,高并发下超卖) $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 单个命令天然原子性,比如decrhDecr,直接用于扣库存:

// 秒杀扣库存核心代码(原子操作,无超卖) 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 脚本保证原子性:

// 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 事务不支持回滚,适合无逻辑依赖的批量操作,秒杀场景用得少:

$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

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

// 缓存空值示例 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

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

// 互斥锁示例(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;

这里是不务正业的程序员

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/19 7:38:59

OpenAI gpt-oss-20b发布:部署与优化全指南

OpenAI gpt-oss-20b部署与优化实战指南 你有没有遇到过这样的困境&#xff1a;想用大模型做本地推理&#xff0c;却发现动辄上百GB显存需求根本无法落地&#xff1f;或者企业希望私有化部署AI能力&#xff0c;却被闭源模型的授权限制卡住脖子&#xff1f;就在最近&#xff0c;O…

作者头像 李华
网站建设 2025/12/19 2:35:16

适当过滤Window event log 输入Splunk

1: 如果window server 比较多的话,那么eventlog 是会很多的,那么可以根据event code 来过滤,具体的设置: 先去DS (deployment server 上去查到这个index 的inputs.conf 文件,然后 index=abc EventCode IN (4658,4656,4690) | timechart span=1m count by EventCode 可以…

作者头像 李华
网站建设 2025/12/20 2:29:32

【企业级数据治理新范式】:基于混合检索的Dify数据源管理实战手册

第一章&#xff1a;企业级数据治理的演进与挑战随着数字化转型的深入&#xff0c;企业级数据治理已从传统的数据管理演变为支撑业务决策、合规运营和智能化创新的核心战略。早期的数据治理主要聚焦于数据质量与元数据管理&#xff0c;而如今则需应对多源异构数据、实时处理需求…

作者头像 李华
网站建设 2026/1/11 13:19:12

【Dify音视频开发秘籍】:突破1.7.0版本音频时长限制的3大核心技术

第一章&#xff1a;Dify 1.7.0 的音频时长限制Dify 1.7.0 版本在处理音频输入时引入了明确的时长约束机制&#xff0c;旨在优化系统资源调度并提升响应效率。该版本默认将单次上传或处理的音频文件时长上限设定为 300 秒&#xff08;即 5 分钟&#xff09;&#xff0c;超出此限…

作者头像 李华
网站建设 2025/12/16 20:00:57

【R Shiny多模态缓存策略】:揭秘高性能交互式应用背后的底层优化逻辑

第一章&#xff1a;R Shiny多模态缓存策略的核心价值在构建交互式数据应用时&#xff0c;R Shiny 常面临计算密集型操作带来的性能瓶颈。多模态缓存策略通过整合内存、磁盘与外部存储机制&#xff0c;显著提升响应速度并降低重复计算开销。缓存机制的类型对比 内存缓存&#xf…

作者头像 李华