Redis 应用实战

Redis 系统讲解第二篇:怎么把 Redis 用对。01 篇已经给出缓存三兄弟的速查结论,这一篇把每个解法讲透——为什么有效、什么条件下失效、代码长什么样。

缓存读写策略全景

三种经典读写策略,不是三选一的平级选项,而是各有定位:

策略 流程 适用 代价
Cache Aside(旁路缓存) 读:先缓存后库;写:先更库再删缓存 最通用,日常默认 写后短时间内缓存与库可能短暂不一致
Read/Write Through 应用只跟缓存层说话,缓存层自己管库 需要缓存服务支持,少见于手写 实现复杂
Write Behind 写只进缓存,异步批量刷库 超高写入(点赞数) 缓存挂了丢数据,慎用

为什么是"删缓存"而不是"更新缓存":并发两个写,更新缓存的顺序可能与数据库提交顺序相反,把旧值写回缓存且再也不会被修正;删掉让下次读自然回源,天然收敛。

为什么是"先更库"而不是"先删缓存":先删后更的窗口里,另一个读请求会从库里拿旧值回填缓存,旧值会一直住到下次过期;先更库再删,最坏也只是短暂不一致。(书里的严谨版本:还有"延迟双删"和"订阅 binlog 异步删"两种增强,见下)

一致性方案 做法 适用
先更库再删缓存 日常默认 绝大多数场景
延迟双删 删缓存 → 更库 → 睡几百毫秒再删一次 读多写多、怕回填旧值
订阅 binlog 异步删 Canal 等中间件监听 MySQL binlog 触发删缓存 要求最终一致性的中大型系统

缓存三兄弟:解法的适用条件

01 篇是结论表,这里记"为什么":

  • 穿透(查不存在的数据):布隆过滤器的原理是"用多个 hash 位标记存在性"——说"不存在"一定不存在,说"可能存在"要再查库确认。所以它能挡掉绝大多数恶意 id,代价是误判率(存在性判断有假阳性)与不能删除元素。
  • 击穿(热点 key 过期瞬间):互斥锁的代码骨架——拿到锁的那个请求去回源重建缓存,其他人短暂等待:
val = r.get(key)
if val is None:
    # 只放一个请求进去重建;setnx 成功者干活,其余人睡 50ms 重试
    if r.set(f"lock:{key}", "1", nx=True, ex=10):
        data = db.query(key)
        r.set(key, serialize(data), ex=3600)
        r.delete(f"lock:{key}")
    else:
        time.sleep(0.05)
        return get_from_cache_or_db(key)   # 递归重试
  • 逻辑过期的思路相反:缓存永不物理过期,value 里带过期时间字段,读到"已过期"的再异步重建——适合重建成本极高的热点数据。
  • 雪崩(大批 key 同时过期 / Redis 整体宕机):过期时间加随机是工程上最便宜的一招;多级缓存(本地 Caffeine/进程内 dict → Redis → DB)是架构级的答案;宕机靠哨兵/集群 + 限流降级兜底。

分布式锁:从玩具到能上生产

01 篇的 SET NX EX 是玩具版,能上生产还差三件事:

  1. 锁误删:A 拿锁后超时自动释放,B 抢到锁,A 干完活 DEL 把 B 的锁删了。解法:value 存唯一 token,删锁前用 Lua 脚本校验"是自己的锁才删"(GET+DEL 必须原子):
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end
  1. 锁续期:业务没跑完锁先过期。Redisson 的看门狗(Java)自动续期;Python 生态用 redis-py + 定时续期线程,或 redlock-py
  2. Redlock 的争议:多节点 Redlock 算法在分布式系统界(Martin Kleppmann 的著名质疑)有安全性争议——单实例锁 + 容忍极小概率失效,对绝大多数业务足够;要强一致锁,考虑用 ZooKeeper/etcd。

限流:三种算法的 Redis 实现

算法 Redis 实现 特点
固定窗口 INCR + EXPIRE,超过阈值拒绝 简单;窗口边界处可能放过 2 倍流量
滑动窗口 ZSet 存时间戳,ZREMRANGEBYSCORE 清旧 + ZCARD 计数 精确,内存随请求量涨
令牌桶 Lua 脚本里按时间生成令牌 + 扣减 允许突发,最通用

固定窗口最小骨架:

n = r.incr(f"rate:{uid}:{int(time.time()) // 60}")
if n == 1:
    r.expire(f"rate:{uid}:{int(time.time()) // 60}", 65)
if n > 100:
    raise TooManyRequests()

💡 真正的限流器 Lua 必须把"读-判-写"包成原子操作,否则并发下计数失真——和分布式锁删锁是同一个道理:多步操作在 Redis 里要用 Lua 合成原子

秒杀骨架(把上面全部串起来)

1. 预热:把库存数 SET 进 Redis(SET stock:sku1 100)
2. 削峰:令牌桶/滑动窗口限流,拦掉绝大部分流量
3. 扣减:Lua 脚本"判断库存 > 0 就 DECR"原子执行(防超卖)
4. 异步:扣减成功者发消息进 Stream,后台慢慢创建订单落 MySQL

单靠 MySQL 扛秒杀是灾难,Redis 扛流量、MySQL 扛最终数据——这是"为什么后端必学 Redis"的最直观答案。


⬅️ 02-Redis 核心数据结构与命令 🏠 00-数据库 ➡️ 04-Redis 持久化与高可用