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 | ⭐⭐⭐⭐⭐ |
| 验证码 / Token | String | ⭐⭐⭐⭐ |
| 商品浏览量 / 点赞数 | 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 1PHP 调用:
$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
endPHP:
$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 | 签到、活动参与 |
| HyperLogLog | UV 统计 |
重要:消息队列、延迟任务请交给 Kafka / RabbitMQ,Redis 只适合极轻量的内部通知。
十八、Key 命名规范与 TTL 建议
命名规范
业务:模块:标识
例:goods:detail:10001, stock:sku:20001, cart:user:10001TTL 参考
| 数据 | TTL |
|---|---|
| 商品详情 | 30~120 分钟(+随机) |
| 分类缓存 | 1~6 小时 |
| 不存在缓存 | 30~120 秒 |
| 验证码 | 5 分钟 |
| 短信限制 | 60 秒 |
| 登录 Token | 7~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 个核心点:
- 商品缓存(Cache Aside)
- 购物车(Hash)
- 防重复提交(SET NX EX)
- 分布式锁(SET NX EX + Lua 释放)
- Lua 原子扣库存
- 缓存穿透/击穿/雪崩应对