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 还能做很多:

8. 何时不要用 Redis

说了这么多 Redis 的好,也要清醒——它不是万能:

一句话:Redis 是"内存加速层",不是"主数据库"。和 MySQL/MongoDB 配合使用,才能发挥最大价值。

系列总结

恭喜你完成了 Redis 入门系列的全部 11 篇!回顾一下学到的:

掌握这些,你已经能独立设计一个带缓存的 Web 服务、用 Redis 解决大多数"高并发、低延迟"的业务问题。下一步建议:读 Redis 官方文档(redis.io)、看《Redis 设计与实现》(黄健宏)、在生产环境实战——理论结合实际,才是真正掌握的不二法门。

← 上一篇 Redis 事务

← 返回 Redis 教程目录

✈️💬