Redis 缓存与后端性能优化:策略、TTL 与穿透防护

Redis 是一个高性能的内存数据结构服务器,官方定位为"data structure server",从缓存、队列到事件处理都能胜任。依据 Redis 官方文档,本文聚焦后端最常用的场景:缓存与性能优化,并给出缓存穿透、击穿、雪崩的防护方案。如果你正在用 Node.js ExpressPython FastAPILaravel 做后端,Redis 几乎都是标配。

一、核心数据类型

Redis 提供了丰富的原生数据类型,选对类型能事半功倍:

类型 特点 典型用途
String 最基础,字节序列 缓存简单值、计数器(INCR)
Hash 字段-值对集合 缓存对象字段(用户资料)
List 插入有序的字符串列表 简单消息队列(LPUSH/BRPOP)
Set 唯一元素集合,O(1) 判存 标签、去重、交集并集
Sorted Set 带分数的有序集合 排行榜、延迟队列
Stream 追加型日志结构 事件流、消息管道

字符串是缓存的主力;Hash 适合按字段读写对象,避免整体序列化;Sorted Set 用于排行榜和基于分数的排序。生产环境还常用 HyperLogLog 做基数统计、Bloom Filter 做布隆过滤。

二、缓存模式与 TTL

最常用的缓存模式是 Cache-Aside(旁路缓存)

  1. 读请求先查 Redis,命中直接返回;
  2. 未命中则查数据库,写回 Redis 并设置 TTL,再返回;
  3. 写请求更新数据库后,删除或更新对应缓存。

TTL(过期时间)是缓存设计的灵魂。官方文档强调要按数据变化频率设置合理的过期时间:热点配置可以几小时甚至永久,用户资料几分钟,验证码几十秒。TTL 太短缓存形同虚设,太长又会让数据陈旧。可以用 SET key value EX secondsEXPIRE 命令设置。

三、三大缓存问题的防护

  • 缓存穿透:请求查一个不存在的 key,每次都打到数据库。解法:对不存在的 key 也缓存一个短 TTL 的空值;或用 Bloom Filter 在查询前快速判断 key 是否存在。
  • 缓存击穿:某个热点 key 过期瞬间,大量请求同时打到数据库。解法:加互斥锁(SETNX)只让一个请求回源,或对热点数据用"逻辑过期"。
  • 缓存雪崩:大量 key 在同一时间过期,数据库瞬时压力剧增。解法:为 TTL 增加随机抖动(如 ±10%),避免集中失效。

一个改造案例:商品详情页

用一个最典型的场景说明收益。某电商详情页每秒 2000 次查询,全部打到 MySQL;加上 Redis 缓存后,热点商品 90% 命中缓存,数据库 QPS 从 2000 降到约 200,P99 响应从 120ms 降到 8ms。做法很简单:

# 启动本地 Redis(Docker)
docker run -d --name redis -p 6379:6379 redis:7

# 验证连接
redis-cli ping
# -> PONG
# Python 侧 Cache-Aside
import redis, json, random

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

def get_product(product_id):
    key = f'product:{product_id}'
    cached = r.get(key)
    if cached:
        return json.loads(cached)
    data = db.query_one('SELECT * FROM products WHERE id=%s', product_id)
    if data is None:
        r.setex(key, 60, json.dumps(None))                          # 防空值,缓解穿透
    else:
        r.setex(key, 300 + random.randint(-30, 30), json.dumps(data))  # TTL 抖动防雪崩
    return data

这里有两个细节很关键:不存在的商品也缓存一个 60 秒空值,避免"查了不存在的 id"每次都打库;TTL 在 300 秒基础上随机 ±10%,避免同一批商品同时过期。改动量不大,却直接决定了高峰期数据库能否扛得住。

缓存更新策略对比

策略 做法 一致性 适用
Cache-Aside 读时回填,写时删缓存 最终一致 最通用,推荐默认
Write-Through 写库同时写缓存 较强 读多写也多的数据
Write-Behind 只写缓存,异步落库 弱,有丢失风险 高写入、可容忍丢失
删除 vs 更新 更新后删 key 而非直接 set 删更安全 避免旧值回写

实践中优先"先更新数据库,再删除缓存",并给删除加上短暂延迟,降低"删了又被旧值写回"的竞态概率。高并发扣库存等操作建议用 Lua 保证原子性:

-- 扣减库存:原子执行,避免超卖
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
if stock <= 0 then return -1 end
redis.call('DECR', KEYS[1])
return stock - 1

参考:Redis 官方数据类型文档 https://redis.io/docs/latest/develop/data-types/、Redis 命令参考 https://redis.io/docs/latest/commands/

四、Session 存储

传统方案把 Session 存在单机内存,多实例部署时会丢会话。用 Redis 存 Session 可以做到集中式、共享、带过期

  • 用户登录后把 Session 数据写入 Redis,key 用会话 ID,TTL 即会话有效期;
  • 多台应用服务器共享同一份 Session,横向扩容不再丢失登录态;
  • 支持"滑动过期",活跃用户自动续期。

Laravel 自带 redis 的 Session 驱动,一行配置即可切换;Express 用 connect-redis 中间件;FastAPI 可以配合 Starlette 的 RedisSessionMiddleware。缓存键的设计规范可参考 缓存 Key 设计策略

五、实践建议

  • 缓存读多写少、一致性要求不高的数据:如商品详情、配置、统计值。
  • 缓存与数据库双写注意一致性:先更新数据库再删缓存,配合短暂延迟通常可接受。
  • 监控命中率:命中率长期偏低说明缓存价值有限,需要调 TTL 或换缓存对象。
  • 内存与淘汰策略:合理设置 maxmemoryallkeys-lru 等淘汰策略,防止内存打满。

16IDC 观察

对独立开发者和小团队,Redis 是性价比极高的性能优化手段:一个内存数据库同时解决缓存、Session、排行榜和简单队列。但缓存引入的是"复杂度",穿透、击穿、雪崩这三个经典问题只要上线前想清楚,生产环境就能少很多深夜告警。更完整的后端工程实践见 后端对接 分类。

原文来源:https://redis.io/docs/latest/develop/data-types/