第8篇:Redis缓存设计与缓存问题

📚 文章概述

缓存是Redis最重要的应用场景之一。合理的缓存设计可以大幅提升系统性能,但不当的缓存使用也会带来各种问题。本文将深入讲解缓存设计模式、缓存穿透、缓存击穿、缓存雪崩等经典问题及其解决方案,帮助读者设计出高性能、高可用的缓存架构。


一、理论部分

1.1 缓存设计模式

1.1.1 Cache-Aside模式(旁路缓存)

工作流程:
Client Cache Database 读取数据 返回数据 未找到 查询数据库 返回数据 写入缓存 alt [缓存命中] [缓存未命中] 写操作 更新数据 成功 删除缓存 Client Cache Database

特点:

  • 应用负责缓存和数据库的读写
  • 缓存和数据库独立
  • 实现简单,灵活性高
1.1.2 Read-Through模式(读穿透)

工作流程:
Client Cache Database 读取数据 返回数据 查询数据库 返回数据 写入缓存 返回数据 alt [缓存命中] [缓存未命中] Client Cache Database

特点:

  • 缓存服务负责从数据库加载数据
  • 对应用透明
  • 需要缓存服务支持
1.1.3 Write-Through模式(写穿透)

工作流程:
Client Cache Database 写入数据 写入数据库 成功 更新缓存 成功 Client Cache Database

特点:

  • 同时更新缓存和数据库
  • 数据一致性高
  • 写性能较低
1.1.4 Write-Behind模式(写回)

工作流程:
Client Cache Database 写入数据 更新缓存 成功(立即返回) 异步写入 异步写入数据库 成功 Client Cache Database

特点:

  • 先更新缓存,异步写入数据库
  • 写性能高
  • 可能丢失数据
1.1.5 模式对比
模式 读性能 写性能 一致性 复杂度
Cache-Aside
Read-Through
Write-Through
Write-Behind 很高

1.2 缓存穿透

1.2.1 问题描述

缓存穿透场景:
未命中 不存在 客户端请求 查询缓存 查询数据库 返回空 不写入缓存 恶意请求 大量不存在的数据 缓存穿透 数据库压力大

问题特征:

  • 查询不存在的数据
  • 缓存和数据库都不命中
  • 大量请求直接打到数据库
1.2.2 解决方案

方案1:缓存空值
查询不存在数据 缓存空值 设置短过期时间 后续请求命中缓存

实现:

python 复制代码
def get_user(user_id):
    # 先查缓存
    user = cache.get(f'user:{user_id}')
    if user is not None:
        return user if user != '' else None
    
    # 查数据库
    user = db.get_user(user_id)
    if user:
        cache.set(f'user:{user_id}', user, 3600)
    else:
        # 缓存空值,短过期时间
        cache.set(f'user:{user_id}', '', 60)
    
    return user

方案2:布隆过滤器
不存在 可能存在 未命中 请求 布隆过滤器 直接返回 查询缓存 查询数据库

实现:

python 复制代码
from redisbloom import Client

rb = Client(host='localhost', port=6379)

# 初始化布隆过滤器
rb.bfCreate('users', 0.01, 1000000)

# 添加数据
rb.bfAdd('users', user_id)

# 检查存在性
if rb.bfExists('users', user_id):
    # 可能存在,继续查询
    user = get_user(user_id)
else:
    # 一定不存在
    return None

1.3 缓存击穿

1.3.1 问题描述

缓存击穿场景:
Client1 Client2 Client3 Cache Database 热点数据过期 查询热点数据 未命中 查询数据库 查询热点数据 未命中 查询数据库 查询热点数据 未命中 查询数据库 大量并发请求 Client1 Client2 Client3 Cache Database

问题特征:

  • 热点数据过期
  • 大量并发请求
  • 数据库压力瞬间增大
1.3.2 解决方案

方案1:互斥锁
Client1 Client2 Cache Database 查询数据 未命中 获取锁 获取成功 查询数据 未命中 获取锁 获取失败 查询数据库 返回数据 写入缓存 释放锁 等待后重试 返回数据 Client1 Client2 Cache Database

实现:

python 复制代码
import redis
import time

def get_user_with_lock(user_id):
    # 先查缓存
    user = cache.get(f'user:{user_id}')
    if user:
        return user
    
    # 尝试获取锁
    lock_key = f'lock:user:{user_id}'
    if cache.set(lock_key, '1', nx=True, ex=10):
        try:
            # 查数据库
            user = db.get_user(user_id)
            if user:
                cache.set(f'user:{user_id}', user, 3600)
            return user
        finally:
            cache.delete(lock_key)
    else:
        # 等待后重试
        time.sleep(0.1)
        return get_user_with_lock(user_id)

方案2:永不过期 + 异步更新

python 复制代码
def get_user_never_expire(user_id):
    # 缓存永不过期
    user = cache.get(f'user:{user_id}')
    if user:
        # 异步检查是否需要更新
        if time.time() - user['update_time'] > 3600:
            # 异步更新
            async_update_user(user_id)
        return user
    
    # 首次加载
    user = db.get_user(user_id)
    if user:
        cache.set(f'user:{user_id}', {
            'data': user,
            'update_time': time.time()
        })
    return user

1.4 缓存雪崩

1.4.1 问题描述

缓存雪崩场景:
大量缓存同时过期 大量请求 缓存未命中 请求数据库 数据库压力激增 数据库崩溃 服务不可用

问题特征:

  • 大量缓存同时过期
  • 大量请求打到数据库
  • 可能导致数据库崩溃
1.4.2 解决方案

方案1:过期时间随机化

python 复制代码
import random

def set_cache(key, value, base_ttl=3600):
    # 基础过期时间 + 随机时间(0-600秒)
    ttl = base_ttl + random.randint(0, 600)
    cache.set(key, value, ttl)

方案2:多级缓存
未命中 未命中 请求 本地缓存 L1 Redis缓存 L2 数据库

方案3:缓存预热

python 复制代码
def cache_warmup():
    # 系统启动时预热热点数据
    hot_keys = get_hot_keys()
    for key in hot_keys:
        data = db.get_data(key)
        cache.set(key, data, 3600)

方案4:限流降级

python 复制代码
from redis import Redis

def get_user_with_limit(user_id):
    # 限流:每秒最多100个请求
    if not rate_limit('user_query', 100, 1):
        # 降级:返回默认值或错误
        return get_default_user()
    
    return get_user(user_id)

1.5 缓存更新策略

1.5.1 更新策略对比
策略 优点 缺点 适用场景
先更新数据库,再删除缓存 简单 可能短暂不一致 一般场景
先删除缓存,再更新数据库 简单 可能短暂不一致 一般场景
先更新数据库,再更新缓存 一致性高 可能浪费更新 一致性要求高
延迟双删 一致性较好 实现复杂 一致性要求高
1.5.2 延迟双删策略

流程:
Client Cache Database 删除缓存 更新数据库 成功 延迟一段时间 再次删除缓存 确保一致性 Client Cache Database

实现:

python 复制代码
import threading
import time

def update_user(user_id, data):
    # 第一次删除缓存
    cache.delete(f'user:{user_id}')
    
    # 更新数据库
    db.update_user(user_id, data)
    
    # 延迟删除缓存
    def delayed_delete():
        time.sleep(1)
        cache.delete(f'user:{user_id}')
    
    threading.Thread(target=delayed_delete).start()

1.6 缓存预热与降级

1.6.1 缓存预热

预热策略:
系统启动 加载热点数据 写入缓存 系统就绪 定时任务 更新热点数据

1.6.2 缓存降级

降级策略:
直接查数据库 返回默认值 返回错误 缓存异常 降级策略 性能下降 功能降级 服务降级


二、实践指南

2.1 缓存设计实践

2.1.1 缓存键设计

键命名规范:

复制代码
业务:模块:功能:标识

示例:

python 复制代码
# 用户信息
user:info:{user_id}

# 商品详情
product:detail:{product_id}

# 订单列表
order:list:{user_id}:{page}
2.1.2 缓存过期时间设计

过期时间策略:

  • 热点数据:较长过期时间(1-24小时)
  • 普通数据:中等过期时间(10-60分钟)
  • 实时性要求高:短过期时间(1-10分钟)

2.2 问题解决方案实践

2.2.1 防止缓存穿透
python 复制代码
def get_user_safe(user_id):
    # 使用布隆过滤器
    if not bloom_filter.exists('users', user_id):
        return None
    
    # 查询缓存
    user = cache.get(f'user:{user_id}')
    if user is not None:
        return user if user != '' else None
    
    # 查询数据库
    user = db.get_user(user_id)
    if user:
        cache.set(f'user:{user_id}', user, 3600)
    else:
        # 缓存空值
        cache.set(f'user:{user_id}', '', 60)
    
    return user
2.2.2 防止缓存击穿
python 复制代码
def get_hot_data(key):
    # 使用互斥锁
    lock_key = f'lock:{key}'
    if cache.set(lock_key, '1', nx=True, ex=10):
        try:
            data = cache.get(key)
            if not data:
                data = db.get_data(key)
                cache.set(key, data, 3600)
            return data
        finally:
            cache.delete(lock_key)
    else:
        # 等待后重试
        time.sleep(0.1)
        return cache.get(key)
2.2.3 防止缓存雪崩
python 复制代码
import random

def set_cache_safe(key, value, base_ttl=3600):
    # 随机过期时间
    ttl = base_ttl + random.randint(0, 600)
    cache.set(key, value, ttl)

三、总结

3.1 关键知识点回顾

  1. 缓存设计模式

    • Cache-Aside:最常用
    • Read-Through/Write-Through:一致性高
    • Write-Behind:性能高
  2. 缓存问题

    • 缓存穿透:查询不存在数据
    • 缓存击穿:热点数据过期
    • 缓存雪崩:大量缓存同时过期
  3. 解决方案

    • 穿透:布隆过滤器、缓存空值
    • 击穿:互斥锁、永不过期
    • 雪崩:随机过期、多级缓存

3.2 最佳实践

  1. 合理设计缓存键
  2. 设置合适的过期时间
  3. 使用布隆过滤器防止穿透
  4. 使用互斥锁防止击穿
  5. 随机化过期时间防止雪崩

下一篇预告: 第9篇将深入讲解Redis分布式锁与分布式ID,包括分布式锁实现、Redlock算法和分布式ID生成策略。


相关推荐
LeeZhao@2 小时前
【狂飙全模态】狂飙AGI-智能视频生成助手
人工智能·redis·语言模型·音视频·agi
qq_348231852 小时前
Redis 事务(MULTI/EXEC)与 Lua 脚本的核心区别
数据库·redis·lua
TT哇2 小时前
【@NotBlank】@NotBlank与@NotEmpty与@NotNull区别
java·开发语言
mozhiyan22 小时前
Spring Tool Suite4(STS)下载安装保姆级教程(附安装包)
java·spring·eclipse·sts4·sts4下载教程
用户0332126663672 小时前
Java 读取或删除 Excel 文件文档属性:Spire.XLS for Java 实用指南
java
忘记9262 小时前
GET 请求与 POST 请求的核心区别
java
没有bug.的程序员2 小时前
JVM 与 Docker:资源限制的真相
java·jvm·后端·spring·docker·容器
lkbhua莱克瓦242 小时前
IO流——打印流
java·开发语言·前端·学习方法
赵得C3 小时前
软件设计师前沿考点精讲:新兴技术与性能优化实战
java·开发语言·分布式·算法·设计模式·性能优化