博客 / 詳情

返回

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 緩存操作,重點掌握秒殺常用的 String Hash List

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 單個命令天然原子性,比如 decr hDecr,直接用於扣庫存:

// 秒殺扣庫存核心代碼(原子操作,無超賣)

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;
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.