Cache Aside 模式

Cache Aside 模式

什么是 Cache Aside 模式

Cache Aside(旁路缓存)是目前最常见的缓存策略,也是 Redis 作为缓存层时最广泛使用的模式。它的核心思想是:应用程序直接管理缓存和数据库的读写,缓存层不主动同步数据

读操作流程

Cache Aside 的读操作非常简单直接:

应用程序读取数据:
1. 先查缓存(Redis)
   ├── 命中 → 直接返回缓存数据(快路径)
   └── 未命中 → 继续
2. 查询数据库
3. 将查询结果写入缓存(设置过期时间)
4. 返回数据
def read_data(key):
    # 1. 查缓存
    data = redis.get(key)
    if data is not None:
        return data

    # 2. 缓存未命中,查数据库
    data = db.query("SELECT * FROM data WHERE id = ?", key)
    if data is not None:
        # 3. 写入缓存(设置过期时间)
        redis.setex(key, 3600, data)
    else:
        # 可选:缓存空对象防止穿透
        redis.setex(key, 30, "NULL")

    # 4. 返回数据
    return data

写操作流程

Cache Aside 的写操作采用先更新数据库,后删除缓存的策略:

应用程序更新数据:
1. 更新数据库
2. 删除缓存(而非更新缓存)
def write_data(key, new_value):
    # 1. 更新数据库
    db.execute("UPDATE data SET value = ? WHERE id = ?", new_value, key)

    # 2. 删除缓存
    redis.delete(key)
    # 注意:不是 redis.set(key, new_value)!
}

为什么写操作是”删除缓存”而非”更新缓存”

问题一:写操作并非总是”写最终值”

考虑一个”++”或”–“操作(如点赞数、库存量):

# 错误做法:更新缓存
def increment_likes(post_id):
    # 更新数据库
    db.execute("UPDATE posts SET likes = likes + 1 WHERE id = ?", post_id)
    # 更新缓存 —— 缓存中是"当前值",但可能是"中间值"
    new_likes = redis.incr(f"post:{post_id}:likes")
    # 问题:缓存的值总是落后或超前于数据库

更常见的情况是更新了复杂对象:

# 更新对象中的一部分字段
def update_user_avatar(user_id, new_avatar_url):
    db.execute("UPDATE users SET avatar = ? WHERE id = ?", new_avatar_url, user_id)
    # 如果"更新缓存":需要先查询完整对象再合并字段 → 两次操作
    # 如果"删除缓存":直接删除,下次读请求重建 → 一次操作
    redis.delete(f"user:{user_id}")  # 简单高效

问题二:避免写操作的并发覆盖

之前已经讨论过:

写线程 A:更新 DB → 更新缓存(V1 → V2)
写线程 B:更新 DB → 更新缓存(V2 → V3)
写线程 A:更新 DB 完成,但...
(如果 A 的缓存更新比 B 慢)

结果:缓存中是 V2(A 的旧值),数据库中是 V3(B 的新值)

删除缓存比更新缓存更安全,因为下次读操作一定会获取最新值。

Cache Aside 的优缺点

优点

特性 说明
实现简单 读写的逻辑清晰,易于理解和维护
数据安全 删除缓存而非更新,避免了并发覆盖
最终一致 配合过期时间,自动恢复一致性
性能好 读操作先查缓存,写操作直接写 DB

缺点

特性 说明
一致性弱 写 DB 到删缓存之间有短暂不一致窗口
缓存命中率 写操作频繁会频繁删除缓存,降低命中率
缓存击穿 缓存失效瞬间大量并发打到 DB
无自动同步 数据变化需要应用层手动操作

Cache Aside 的防护措施

防止缓存击穿

def read_data_with_lock(key):
    data = redis.get(key)
    if data is not None:
        return data

    # 互斥锁防止击穿
    if redis.set(f"lock:{key}", "1", nx=True, ex=5):
        try:
            # 双重检查
            data = redis.get(key)
            if data is not None:
                return data

            data = db.query(key)
            redis.setex(key, 3600, data)
            return data
        finally:
            redis.delete(f"lock:{key}")
    else:
        time.sleep(0.05)
        return redis.get(key)  # 等待后重试

防止缓存穿透

def read_data_with_bloom(key):
    # 布隆过滤器检查
    if not bloom_filter.contains(key):
        return None  # 一定不存在

    # 正常 Cache Aside 流程
    data = redis.get(key)
    if data is not None:
        return data

    data = db.query(key)
    if data is not None:
        redis.setex(key, 3600, data)
    else:
        redis.setex(key, 30, "NULL")  # 缓存空对象
    return data

防止缓存雪崩

def read_data_with_random_ttl(key):
    data = redis.get(key)
    if data is not None:
        return data

    data = db.query(key)
    if data is not None:
        # 随机过期时间,防止雪崩
        ttl = 3600 + random.randint(-300, 300)
        redis.setex(key, ttl, data)
    return data

Cache Aside 的变体

Read-Through

和 Cache Aside 相比,Read-Through 将”缓存读取失败后查数据库”的逻辑封装到缓存库中:

Cache Aside:应用层先查缓存,未命中时应用层查数据库
Read-Through:应用层只调用缓存库,缓存库内部处理未命中的逻辑

Write-Through

写操作时,缓存库自动将数据同时写入缓存和数据库:

Cache Aside:应用层主动更新 DB + 主动删除缓存
Write-Through:应用层只写缓存,缓存库同步写入 DB

Write-Behind

写操作先写入缓存,缓存库异步批量写入数据库(延迟写):

Cache Aside:实时写入 DB
Write-Behind:先写缓存,后台异步写 DB

实际应用中的 Cache Aside

class CacheAside(object):
    """Cache Aside 模式封装"""

    def __init__(self, redis_client, db_client, ttl=3600):
        self.redis = redis_client
        self.db = db_client
        self.ttl = ttl

    def get(self, key, loader_func):
        """
        Cache Aside 读取
        loader_func: 从数据库加载数据的函数
        """
        # 1. 查缓存
        data = self.redis.get(key)
        if data is not None:
            return data

        # 2. 缓存未命中,查数据库
        data = loader_func()
        if data is not None:
            # 3. 写入缓存
            ttl = self.ttl + random.randint(-300, 300)
            self.redis.setex(key, ttl, data)
        else:
            # 4. 缓存空对象(防止穿透)
            self.redis.setex(key, 30, "NULL")

        return data

    def set(self, key, value, db_func):
        """
        Cache Aside 写入
        db_func: 更新数据库的函数
        """
        # 1. 更新数据库
        db_func(value)

        # 2. 删除缓存
        self.redis.delete(key)

面试话术

当面试官问”你是如何保证缓存和数据库一致性的”,可以这样回答:

“我们使用的是标准的 Cache Aside 模式。读取时先查缓存,未命中再查数据库并回写缓存;写入时先更新数据库,然后删除缓存。这样做的原因是:删除缓存比更新缓存更安全,能避免并发写操作的覆盖问题。每次缓存被删除后,下一次读操作会自动加载最新数据,保证了最终一致性。在这个基础上,我们还设置了随机的缓存过期时间来防止缓存雪崩,并在高并发场景下使用互斥锁来防止缓存击穿。”

总结

Cache Aside 是 Redis 缓存的最标准、最推荐的使用模式。它的核心设计理念是:让应用层显式管理缓存的生命周期,通过”先写 DB 后删缓存”的机制保证数据最终一致。虽然它不能提供强一致性,但在绝大多数业务场景中,Cache Aside 提供的最终一致性 + 高性能组合是最佳选择。

理解 Cache Aside 模式,是掌握 Redis 缓存应用的基础,也是面试中回答”缓存和数据库一致性”问题的标准答案框架。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容