Redis 实战场景
学到这里你已经掌握了 Redis 的全部基础。最后一篇把这些知识组合到真实业务场景——缓存策略与三大问题、分布式会话、排行榜、分布式锁、API 限流。这些是后端面试的高频考点,也是生产环境最常用的 Redis 应用。
1. 缓存与 Cache Aside 策略
"缓存"是 Redis 最常见的用途——把数据库查询结果存到 Redis,下次同样的查询直接返回内存里的副本,大幅降低数据库压力和响应时间。但怎么读写缓存有讲究,最经典的策略叫 Cache Aside:
# Cache Aside:最经典的缓存读写策略
# 读流程:
def get_user(user_id):
# 1. 先读 Redis
data = redis.get(f"user:{user_id}")
if data:
return json.loads(data) # 缓存命中
# 2. 缓存未命中,读数据库
data = db.query("SELECT * FROM users WHERE id = ?", user_id)
# 3. 写回 Redis(设过期时间,关键!)
redis.setex(f"user:{user_id}", 3600, json.dumps(data))
return data
# 写流程(更新数据时):
def update_user(user_id, new_data):
# 1. 先更新数据库
db.update("UPDATE users SET ... WHERE id = ?", user_id, new_data)
# 2. 删除缓存(不是更新缓存!)
redis.delete(f"user:{user_id}")
# 关键点:
# - 写时"删缓存"而不是"更新缓存":避免并发写时数据错乱
# - 缓存一定要设过期时间:兜底防止脏数据
# - 任何时刻数据库是"权威源",Redis 只是镜像几个易错点(后端面试常考):写时删缓存而不是更新缓存(避免并发写时数据错乱)、缓存必须设过期时间(兜底防脏数据)、数据库才是权威源(Redis 只是镜像,丢失要能重建)。除 Cache Aside 外,还有 Read/Write Through、Write Behind 等策略,但 90% 的业务用 Cache Aside 就够了。
2. 缓存三大问题
这是 Redis 缓存的必考点。三个问题表现形式不同,但本质都是"缓存失效时大量请求打到数据库"。
# 缓存三大经典问题
# 1. 缓存穿透(Cache Penetration):查一个根本不存在的 key
# 每次都打到数据库,可能被恶意利用
# 解决方案:
# A. 缓存空值(简单但有内存浪费)
def get_user(uid):
data = redis.get(f"user:{uid}")
if data == "NULL":
return None # 缓存了空值
if data:
return json.loads(data)
data = db.query(...)
if data is None:
redis.setex(f"user:{uid}", 60, "NULL") # 短过期缓存空值
return None
redis.setex(f"user:{uid}", 3600, json.dumps(data))
return data
# B. 布隆过滤器(更省内存):先判断 key 是否可能存在
# 不存在直接返回,不查 DB
# 2. 缓存击穿(Cache Breakdown):某个热点 key 突然过期
# 瞬间大量请求打到数据库
# 解决方案:
# A. 互斥锁(同一时刻只让一个请求查 DB)
def get_hot(key):
data = redis.get(key)
if data:
return data
# 加锁(SET NX EX)
if redis.set(f"lock:{key}", "1", nx=True, ex=10):
try:
data = db.query(...)
redis.setex(key, 3600, data)
return data
finally:
redis.delete(f"lock:{key}")
else:
time.sleep(0.1)
return get_hot(key) # 重试
# B. 热点 key 永不过期(后台异步更新)
# 3. 缓存雪崩(Cache Avalanche):大量 key 同时过期
# 解决方案:
# - 过期时间加随机偏移(redis.setex(key, 3600 + random(0,300), v))
# - 部署 Redis 集群避免单点故障
# - 服务端做熔断/降级(数据库扛不住时直接返回默认值)对照记忆:穿透是"查不存在的"(用空值缓存或布隆过滤器)、击穿是"热点 key 过期"(用互斥锁或永不过期)、雪崩是"大量 key 同时过期"(过期时间加随机)。生产环境通常三者都防:布隆过滤器 + 互斥锁 + 随机过期时间。
3. 分布式 Session 存储
Web 服务部署多台时,Session 不能存在单机内存——用户登录后下次请求可能落到另一台机器,会话就丢了。用 Redis 存 Session 是行业标准方案。
# 分布式 Session 存储(用 hash 类型)
# 用户登录成功后,把会话信息存 Redis
import uuid
def login(user):
session_id = str(uuid.uuid4())
# 用 hash 存,支持单字段更新
redis.hset(f"session:{session_id}", mapping={
"user_id": user.id,
"username": user.name,
"login_at": int(time.time()),
"ip": request.remote_addr
})
redis.expire(f"session:{session_id}", 86400 * 7) # 7 天过期
response.set_cookie("session_id", session_id, httponly=True)
# 每次请求中间件验证
def auth_middleware(request):
session_id = request.cookies.get("session_id")
if not session_id:
return redirect("/login")
session = redis.hgetall(f"session:{session_id}")
if not session:
return redirect("/login") # 过期或不存在
# 续期(活跃用户会话不被清)
redis.expire(f"session:{session_id}", 86400 * 7)
request.user = session
# 登出
def logout(request):
session_id = request.cookies.get("session_id")
redis.delete(f"session:{session_id}")
# 优势:
# - 多台 Web 服务器共享会话(关键是分布式)
# - 自动过期(无需手动清理)
# - 内存读写快,验证成本极低关键设计:用 hash 而不是 string(支持单字段更新,如只改 IP)、每次访问续期(活跃用户不被强制下线)、session_id 用 UUID(不可猜测,防伪造)、cookie 加 httponly(防 XSS 偷取)。生产场景还会配合 Spring Session、express-session 等框架,框架已经把这套封装好。
4. 实时排行榜
把前面学的 zset 组合起来,做一个生产级排行榜系统(日榜、周榜、总榜、附近对手)。
# 实时排行榜(zset 类型,综合应用)
# 1. 玩家得分实时更新
def on_score_change(user_id, delta):
redis.zincrby("leaderboard:daily", delta, user_id)
redis.zincrby("leaderboard:weekly", delta, user_id)
redis.zincrby("leaderboard:alltime", delta, user_id)
# 2. 取前 10 名 + 我的排名
def get_top_and_me(user_id):
pipe = redis.pipeline()
pipe.zrevrange("leaderboard:daily", 0, 9, withscores=True)
pipe.zrevrank("leaderboard:daily", user_id)
pipe.zscore("leaderboard:daily", user_id)
top10, my_rank, my_score = pipe.execute()
return {
"top10": [{"user": u, "score": s} for u, s in top10],
"my_rank": my_rank + 1 if my_rank is not None else None,
"my_score": my_score
}
# 3. 取我附近的对手(我前后各 5 名)
def get_neighbors(user_id):
my_rank = redis.zrevrank("leaderboard:daily", user_id)
if my_rank is None:
return []
start = max(0, my_rank - 5)
end = my_rank + 5
return redis.zrevrange("leaderboard:daily", start, end, withscores=True)
# 4. 每日榜单凌晨归零(用 EXPIRE + 命名规范)
def daily_reset():
# 不直接删,而是换 key 名(应用读时按日期算 key)
yesterday = "leaderboard:" + time.strftime("%Y%m%d")
redis.rename("leaderboard:daily", yesterday)
redis.expire(yesterday, 86400 * 30) # 保留 30 天几个关键技巧:用 pipeline 批量查询(一次返回前 10 + 我的排名 + 我的分数,减少网络往返)、每日榜单用日期命名 + EXPIRE(自动归档 30 天历史)、"我附近的人"用 ZREVRANGE 的相对范围。这套架构可以撑住百万级玩家实时排名——传统数据库几乎做不到。
5. 分布式锁
跨进程互斥的经典场景:订单不能被两个进程同时处理、定时任务不能被多实例重复执行、库存不能超扣。Redis 分布式锁是事实标准。
# 分布式锁(SET NX EX + Lua 释放,生产推荐 Redisson)
# 加锁
import uuid
def acquire_lock(lock_key, ttl=30):
token = str(uuid.uuid4())
# NX: 不存在才设置;EX: 自动过期防死锁
ok = redis.set(lock_key, token, nx=True, ex=ttl)
return token if ok else None
# 释放锁(必须用 Lua 脚本,保证"判断+删除"原子)
RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
def release_lock(lock_key, token):
return redis.eval(RELEASE_SCRIPT, 1, lock_key, token)
# 业务使用
token = acquire_lock("lock:order:1001")
if not token:
raise Exception("其他人正在处理此订单")
try:
process_order(1001)
finally:
release_lock("lock:order:1001", token)
# 生产环境建议:
# 1. 用 Redisson 框架(自带看门狗续期、可重入、公平锁)
# 2. 锁的 TTL 要大于业务最长执行时间
# 3. 不要用 DEL 直接删,会误删别人的锁
# 4. 强一致性要求高的场景考虑 Redlock 算法(多 Redis 实例)生产建议直接用 Redisson(Java)/ redis-py-lock(Python)/ go-redsync(Go)——它们封装了看门狗(自动续期防业务超时)、可重入锁、公平锁、Redlock 算法(多实例防单点故障)等高级特性。自己手写很难覆盖所有边界场景。但面试时往往会让你手写基础版本,所以前面那段代码必须看懂。
6. API 限流
"每个 IP 每分钟最多 100 次请求"——这种限流用 Redis 实现非常自然。常见有三种算法:
# API 限流:固定窗口 + 滑动窗口
# 1. 固定窗口(用 INCR + EXPIRE,最简单)
def allow_fixed_window(ip, limit=100, window=60):
key = f"ratelimit:{ip}:{int(time.time() // window)}"
count = redis.incr(key)
if count == 1:
redis.expire(key, window) # 第一次访问设过期
return count <= limit
# 2. 滑动窗口(用 zset,更精确)
def allow_sliding_window(ip, limit=100, window=60):
key = f"ratelimit:sliding:{ip}"
now = time.time()
pipe = redis.pipeline()
pipe.zadd(key, {ip + str(now): now}) # 加一条记录(用唯一 member)
pipe.zremrangebyscore(key, 0, now - window) # 删除窗口外的旧记录
pipe.zcard(key) # 统计窗口内总数
pipe.expire(key, window)
_, _, count, _ = pipe.execute()
return count <= limit
# 3. 令牌桶(用 list 模拟,允许突发)
def init_token_bucket(bucket, capacity=10, refill_per_sec=1):
# 初始放满令牌
redis.delete(bucket)
redis.rpush(bucket, *range(capacity))
def take_token(bucket, capacity=10, refill_per_sec=1):
# 简化版:实际生产用 Lua 脚本保证原子
refill(bucket, capacity, refill_per_sec)
return redis.lpop(bucket) is not None # 拿到令牌允许请求
# 适用场景:
# 固定窗口:简单计数(每分钟 100 次)
# 滑动窗口:精确限流(避免临界点突发)
# 令牌桶:允许短时突发(Guava RateLimiter 同款)三种算法的取舍:固定窗口最简单但有"临界点突发"(59 秒打 100 次 + 下一秒再 100 次)、滑动窗口精确但内存稍多(每个请求一条 zset 记录)、令牌桶允许突发(Guava RateLimiter 同款,适合"平均 100/分但允许短时 200")。生产环境根据业务严格度选择。
7. 其他常见场景
除了上面 6 个,Redis 还能做很多:
- 计数器/点赞/收藏:
INCR/ZINCRBY,原子且极速。 - 共同关注/共同好友:
SINTER求交集。 - 签到/在线状态:Bitmap,
SETBIT/BITCOUNT,极省内存。 - UV 统计:HyperLogLog,
PFADD/PFCOUNT,12KB 估亿级。 - 附近的人/店铺:Geo,
GEOADD/GEORADIUS。 - 延迟任务:zset + 时间戳 score(订单 30 分钟未支付自动取消)。
- 消息队列:list 简单队列 或 Stream 持久化队列。
- 购物车:hash,field 是商品 ID,value 是数量。
8. 何时不要用 Redis
说了这么多 Redis 的好,也要清醒——它不是万能:
- 数据量大于内存:Redis 装不下时,要么分片要么换数据库,不要"用 Redis 当主库"。
- 强关系型数据:需要 JOIN、外键约束、复杂 SQL 查询,关系数据库仍不可替代。
- 强事务:银行账户、订单支付等强一致性场景,用 MySQL/PostgreSQL 的事务。
- 大文件存储:视频、图片放对象存储,Redis 不适合存大 value(单 key 建议不超过 10KB)。
- 冷数据归档:几乎不访问的数据放硬盘数据库,Redis 是"加速器"不是"仓库"。
一句话:Redis 是"内存加速层",不是"主数据库"。和 MySQL/MongoDB 配合使用,才能发挥最大价值。
系列总结
恭喜你完成了 Redis 入门系列的全部 11 篇!回顾一下学到的:
- 基础概念:Redis 是什么、为什么快、和数据库的关系。
- 环境与命令:安装、redis-cli、配置、五大类型的全部常用命令。
- 工程特性:持久化(RDB/AOF)、发布订阅、事务和 Lua、Stream。
- 实战应用:缓存策略、三大缓存问题、会话、排行榜、分布式锁、限流。
掌握这些,你已经能独立设计一个带缓存的 Web 服务、用 Redis 解决大多数"高并发、低延迟"的业务问题。下一步建议:读 Redis 官方文档(redis.io)、看《Redis 设计与实现》(黄健宏)、在生产环境实战——理论结合实际,才是真正掌握的不二法门。
← 上一篇 Redis 事务
← 返回 Redis 教程目录