Redis 在电商平台中的核心应用场景与实战(PHP + ThinkPHP)

Redis 在电商平台中的核心应用场景与实战(PHP + ThinkPHP)

适用读者:PHP / 电商后端开发,希望掌握 Redis 在真实项目中的高频用法,而非背诵命令。


一、为什么电商项目离不开 Redis

一个典型电商系统的请求链路:

用户 → 商城接口 → 商品/SKU/库存/购物车/订单/优惠券 → MySQL

若所有请求直连 MySQL,会出现:

  • 商品详情:每秒数千次查询 → 数据库压力巨大
  • 秒杀:几万人抢 100 件商品 → 数据库行锁竞争严重
  • 购物车:频繁增删改 → 响应变慢

Redis 的核心价值在于 将高频、高并发、短生命周期、需要原子操作的数据从 MySQL 前拦截,充当高性能缓存与并发控制层。

典型架构:

                    ┌──────────────┐
                    │    Redis     │
                    │ 缓存/库存/锁  │
                    └──────▲───────┘
                           │
用户 → PHP/ThinkPHP → Service
                           │
                    ┌──────▼───────┐
                    │    MySQL     │
                    │  最终业务数据 │
                    └──────────────┘

原则:MySQL 是最终可靠数据源,Redis 是高性能临时数据/缓存/并发控制层,两者分工明确。


二、电商项目最值得掌握的 10 个 Redis 场景

场景Redis 数据结构重要程度
商品详情缓存String / Hash⭐⭐⭐⭐⭐
SKU 库存缓存String⭐⭐⭐⭐⭐
秒杀扣库存String + Lua⭐⭐⭐⭐⭐
购物车Hash⭐⭐⭐⭐⭐
分布式锁String + SET NX EX⭐⭐⭐⭐⭐
防重复下单String + SET NX EX⭐⭐⭐⭐⭐
验证码 / TokenString⭐⭐⭐⭐
商品浏览量 / 点赞数String / Hash⭐⭐⭐⭐
商品排行榜ZSet⭐⭐⭐⭐
接口限流String + INCR + EXPIRE⭐⭐⭐⭐

注:异步任务、延迟订单关闭等消息队列场景,应优先使用 Kafka / RabbitMQ,Redis 仅适合极轻量的内部通知,不推荐在生产环境作为主力 MQ。

推荐学习顺序:

商品缓存 → 购物车 → 库存 → 分布式锁 → 防重复提交 → 秒杀 → 排行榜 → 限流

三、PHP 连接 Redis(生产级封装)

生产环境推荐 phpredis 扩展。

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('your_password');
$redis->select(0);

ThinkPHP 项目应封装为单例服务,避免重复 new Redis():

class RedisService
{
    protected static $redis;

    public static function getRedis()
    {
        if (!self::$redis) {
            self::$redis = new \Redis();
            self::$redis->connect(config('redis.host'), config('redis.port'));
            if (config('redis.password')) {
                self::$redis->auth(config('redis.password'));
            }
            self::$redis->select(config('redis.database', 0));
        }
        return self::$redis;
    }
}

业务中调用:

$redis = RedisService::getRedis();

四、场景一:商品详情缓存(Cache Aside 模式)

4.1 Key 设计规范

goods:detail:{goods_id}

示例:goods:detail:10001

4.2 读取逻辑(先查缓存,再查 DB)

public function getGoodsDetail(int $goodsId)
{
    $redis = RedisService::getRedis();
    $key = "goods:detail:{$goodsId}";

    $cache = $redis->get($key);
    if ($cache !== false) {
        return json_decode($cache, true);
    }

    $goods = Db::name('goods')->where('id', $goodsId)->find();
    if (!$goods) {
        return null;
    }

    // 写入缓存,TTL 1小时(可加随机值防雪崩)
    $ttl = 3600 + random_int(0, 600);
    $redis->setex($key, $ttl, json_encode($goods, JSON_UNESCAPED_UNICODE));
    return $goods;
}

4.3 更新时删除缓存(Cache Aside)

public function updateGoods(int $goodsId, array $data)
{
    Db::startTrans();
    try {
        Db::name('goods')->where('id', $goodsId)->update($data);
        Db::commit();

        $redis = RedisService::getRedis();
        $redis->del("goods:detail:{$goodsId}"); // 删除缓存
        return true;
    } catch (\Throwable $e) {
        Db::rollback();
        throw $e;
    }
}

下次请求会重新查 DB 并重建缓存,保证最终一致性。


五、缓存穿透、击穿、雪崩及应对

5.1 缓存穿透(查不存在的数据)

现象:大量请求 goods_id 不存在,直接打到 DB。

解决:缓存空值(短 TTL)。

if (!$goods) {
    $redis->setex($key, 60, 'NULL'); // 空值只缓存 60 秒
    return null;
}

5.2 缓存雪崩(大量 Key 同时过期)

解决:TTL 加随机偏移。

$ttl = 3600 + random_int(0, 600);

5.3 缓存击穿(热点 Key 过期,瞬间大量请求 DB)

解决:使用分布式锁(见后文),只让一个请求去查 DB,其余等待。


六、场景二:购物车(Hash 结构)

Key:cart:user:{user_id}
Field:sku_id
Value:数量

6.1 添加商品(HINCRBY)

public function addCart(int $userId, int $skuId, int $quantity = 1)
{
    $redis = RedisService::getRedis();
    $key = "cart:user:{$userId}";
    $redis->hIncrBy($key, (string)$skuId, $quantity);
    $redis->expire($key, 86400 * 30); // 30天过期
}

6.2 查询购物车(HGETALL)

public function getCart(int $userId)
{
    $redis = RedisService::getRedis();
    $cart = $redis->hGetAll("cart:user:{$userId}");
    return $cart ?: [];
}

拿到 SKU 列表后,批量查数据库(避免 N+1):

$skuIds = array_keys($cart);
$skuList = Db::name('goods_sku')->whereIn('id', $skuIds)->select();

6.3 修改 / 删除

$redis->hSet($key, $skuId, $newQuantity);
$redis->hDel($key, $skuId);      // 删除某 SKU
$redis->del($key);               // 清空购物车

七、场景三:库存缓存与秒杀扣减(Lua 原子操作)

7.1 初始化库存

$redis->set('stock:sku:20001', 100);

7.2 普通 DECR 的隐患(超卖)

$stock = $redis->get($key);
if ($stock > 0) {
    $redis->decr($key); // 非原子,并发下可能超卖
}

7.3 使用 Lua 脚本原子扣减

local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock then return -1 end
if stock <= 0 then return 0 end
redis.call('DECR', KEYS[1])
return 1

PHP 调用:

$lua = <<<LUA
...(如上脚本)
LUA;
$result = $redis->eval($lua, [$key], 1);
// 返回 1=成功, 0=库存不足, -1=未初始化

秒杀流程:Redis 预扣库存 → 生成订单 → 发送 MQ(Kafka)异步落库,确保最终一致性。


八、场景四:分布式锁(SET NX EX + Lua 释放)

8.1 获取锁

$lockKey = "order:pay:lock:{$orderId}";
$lockValue = bin2hex(random_bytes(16)); // 唯一标识
$locked = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 10]);
if (!$locked) {
    throw new \Exception('订单正在处理中');
}

8.2 安全释放(Lua 校验 value)

if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

PHP:

$redis->eval($lua, [$lockKey, $lockValue], 1);

为什么不能直接 DEL?防止误删其他请求的锁(因业务超时锁自动过期)。


九、场景五:防重复下单(幂等)

前端生成 request_token,后端用 Redis 做幂等控制。

$key = "order:request:{$requestToken}";
$result = $redis->set($key, 1, ['NX', 'EX' => 30]);
if (!$result) {
    throw new \Exception('请勿重复提交订单');
}

注意:不要用 user_id 做 key,否则用户连续买不同商品会被误拦截。


十、场景六:验证码与发送频率限制

10.1 存储验证码

$code = random_int(100000, 999999);
$redis->setex("sms:login:{$mobile}", 300, $code); // 5分钟有效

10.2 校验并删除(防重用)

$cacheCode = $redis->get("sms:login:{$mobile}");
if (!$cacheCode || (string)$cacheCode !== (string)$code) {
    throw new \Exception('验证码无效');
}
$redis->del("sms:login:{$mobile}");

10.3 发送频率限制(60 秒一次)

$key = "sms:send:limit:{$mobile}";
$result = $redis->set($key, 1, ['NX', 'EX' => 60]);
if (!$result) {
    throw new \Exception('请勿频繁发送');
}

十一、场景七:接口限流(固定窗口)

$key = "rate:search:user:{$userId}";
$count = $redis->incr($key);
if ($count === 1) {
    $redis->expire($key, 60);
}
if ($count > 100) {
    throw new \Exception('请求过于频繁');
}

更复杂场景可用滑动窗口或令牌桶,但多数 PHP 项目此简版已够用。


十二、场景八:商品浏览量(异步批量更新 MySQL)

每次访问:

$redis->incr("goods:view:{$goodsId}");

后台定时任务(如每分钟)读取所有浏览量并批量更新 MySQL,再重置 Redis 计数,避免频繁写 DB。


十三、场景九:商品排行榜(ZSet)

13.1 销量增加

$redis->zIncrBy('rank:goods:sales:20260903', 1, $goodsId);

13.2 查询 TOP 10

$rank = $redis->zRevRange('rank:goods:sales:20260903', 0, 9, true);
// 返回 [goods_id => score]

十四、场景十:用户最近浏览(ZSet 按时间戳)

$key = "user:browse:{$userId}";
$redis->zAdd($key, time(), $goodsId);
// 保留最近 100 条
$count = $redis->zCard($key);
if ($count > 100) {
    $redis->zRemRangeByRank($key, 0, $count - 101);
}
// 查询最近 20 条
$goodsIds = $redis->zRevRange($key, 0, 19);

十五、场景十一:优惠券抢券(原子性判断库存 + 用户重复领取)

使用 Lua 同时检查库存、用户是否已领、扣库存、记录用户。

local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock or stock <= 0 then return 0 end
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return -1 end
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1

调用时传入 KEYS[1]=coupon:stock:90001,KEYS[2]=coupon:users:90001,ARGV[1]=user_id。


十六、综合下单流程(Redis 与 MySQL 协作)

用户提交订单(SKU + 数量):

1. 防重复提交(SET NX EX)
2. Lua 预扣 Redis 库存
3. 开启 MySQL 事务
   - 插入订单
   - 条件更新 MySQL 库存(where stock >= quantity)
   - 若失败则回滚并补偿 Redis 库存
4. 提交事务,发送 MQ(Kafka)异步处理后续(积分、优惠券等)

支付回调时使用分布式锁防止重复处理。


十七、Redis 数据类型与业务对照表

类型典型场景
String缓存、库存、验证码、Token、锁、计数器
Hash购物车、对象属性存储
Set优惠券领取用户、标签、去重
ZSet排行榜、最近浏览、延迟任务(轻量)
List(不推荐做 MQ,建议用 Kafka)
Bitmap签到、活动参与
HyperLogLogUV 统计

重要:消息队列、延迟任务请交给 Kafka / RabbitMQ,Redis 只适合极轻量的内部通知。


十八、Key 命名规范与 TTL 建议

命名规范

业务:模块:标识
例:goods:detail:10001, stock:sku:20001, cart:user:10001

TTL 参考

数据TTL
商品详情30~120 分钟(+随机)
分类缓存1~6 小时
不存在缓存30~120 秒
验证码5 分钟
短信限制60 秒
登录 Token7~30 天
分布式锁5~30 秒
防重复提交10~60 秒
购物车15~30 天
秒杀库存活动结束后删除

原则:凡临时数据均设 TTL,避免内存溢出。


十九、PHP 电商项目最需掌握的 Redis 命令清单

String:SET, GET, SETEX, DEL, EXPIRE, TTL, INCR, INCRBY, DECR, DECRBY
Hash:HSET, HGET, HGETALL, HDEL, HINCRBY
Set:SADD, SISMEMBER, SREM, SMEMBERS
ZSet:ZADD, ZINCRBY, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREM
并发控制:SET NX EX, EVAL(Lua)

掌握这些已覆盖 90% 以上业务需求。


二十、学习路线与实战建议

阶段一(会用):String/Hash/Set/ZSet + TTL + INCR → 实现缓存、购物车、验证码、排行榜
阶段二(理解缓存):穿透/击穿/雪崩 + Cache Aside + 一致性
阶段三(高并发):分布式锁 + Lua + 库存扣减 + 防重复 + 限流
阶段四(完整项目):搭建 ThinkPHP + MySQL + Redis 的订单秒杀 Demo,包含商品、SKU、库存、购物车、订单、秒杀、排行榜。

最终架构:

                 Nginx
                   │
                   ▼
              ThinkPHP
                   │
          ┌────────┼─────────┐
          ▼        ▼         ▼
       Redis     MySQL    Kafka(异步)
          │        │         │
          │        │         ▼
          │        │      Consumer
          │        │
          ▼        ▼
      缓存/库存   订单数据
      锁/购物车   最终库存
      限流/排行

结语

Redis 在电商中最值得重点掌握的 6 个核心点:

  1. 商品缓存(Cache Aside)
  2. 购物车(Hash)
  3. 防重复提交(SET NX EX)
  4. 分布式锁(SET NX EX + Lua 释放)
  5. Lua 原子扣库存
  6. 缓存穿透/击穿/雪崩应对