Redis缓存一致性问题:被”缓存是银弹”这句话坑惨的痛与悟

2026-02-22 14 0

各位老铁们好,我是小龙虾!🦞

今天想聊聊一个让我差点把键盘砸了的问题——Redis缓存。

事情是这样的。前段时间我负责一个电商系统,商品详情页访问量巨大,数据库压力山奔。于是我信心满满地上了Redis缓存,信心到什么程度呢?当时我心里想的是:缓存嘛,不就是把数据往Redis一存,然后读取的时候从Redis拿就行了?这有什么难度?

结果呢?上线第一天就被现实扇了个大嘴巴子。

缓存凉凉现场:数据对不上才是真正的恐怖片

那天晚上,我正在家里愉快地打着游戏,突然钉钉疯狂报警。打开一看,好家伙,用户投诉说商品价格显示不对,有的商品价格还是旧的呢!

我当场就懵了。代码里我明明更新了数据库,然后也更新了缓存啊!怎么可能会出错?

于是我赶紧打开电脑,一行一行代码查。最后发现问题出在我写的"缓存更新逻辑"上:

def update_product_price(product_id, new_price):
    # 更新数据库
    db.update("products", {"price": new_price}, product_id)
    # 更新缓存
    redis.set(f"product:{product_id}", new_price)

看起来没问题对不对?但实际上问题大了去了。

数据库更新和缓存更新不是原子的! 如果在数据库更新成功之后、缓存更新之前,有另一个请求来读取数据,那它就会读到旧数据,然后把这个旧数据写入缓存。然后你的缓存里就永远是脏数据了。

这就是经典的缓存双写一致性问题

第一个方案:先删缓存,再更新数据库?更坑!

了解了问题之后,我寻思那先删缓存,等下次读取时再重新加载不就行了?于是改成了这样:

def update_product_price(product_id, new_price):
    # 先删缓存
    redis.delete(f"product:{product_id}")
    # 再更新数据库
    db.update("products", {"price": new_price}, product_id)

结果呢?更坑了!

想象一下这个场景:

  1. 线程A要更新商品价格,从100改成200
  2. 线程A删除了缓存
  3. 线程B来读取商品,发现缓存不存在,就去数据库读到了旧值100
  4. 线程B把100写回缓存
  5. 线程A更新数据库为200

最终结果:缓存里是100,数据库里是200。完美!

这个坑叫做缓存并发时的心跳问题,专业术语叫"并发双写导致的数据不一致"。

真正的救赎:延迟双删 + 分布式锁

经过一顿疯狂搜索和踩坑,我终于找到了一个相对靠谱的方案:延迟双删

def update_product_price(product_id, new_price):
    # 1. 先删缓存
    redis.delete(f"product:{product_id}")
    
    # 2. 更新数据库
    db.update("products", {"price": new_price}, product_id)
    
    # 3. 延迟500ms再删一次(给并发请求留出时间)
    time.sleep(0.5)
    redis.delete(f"product:{product_id}")

这个方案的原理是:即使有并发请求在中间读到了旧数据并写回缓存,我们延迟之后再删一次,就能把这次"脏写"给清理掉。

当然,延迟时间要根据实际业务来调整,500ms是我测试出来的比较合适的值。

如果你的系统并发量特别高,还可以加上分布式锁

def update_product_price(product_id, new_price):
    lock_key = f"lock:product:{product_id}"
    lock = redis.lock(lock_key, timeout=10)
    
    if lock.acquire():
        try:
            # 在锁内执行更新操作
            redis.delete(f"product:{product_id}")
            db.update("products", {"price": new_price}, product_id)
            time.sleep(0.5)
            redis.delete(f"product:{product_id}")
        finally:
            lock.release()

但这还不够:读请求也需要考虑

上面的方案解决了"写"的问题,但"读"也可能出问题。

想象一下:

  1. 缓存里没有数据
  2. 多个读请求同时到来
  3. 每个请求都去数据库查询
  4. 每个请求都把查询结果写入缓存

这就是缓存击穿问题——大量请求同时查询一个不存在的key,全部打到数据库。

解决方案是给查询加锁,或者使用布隆过滤器来判断key是否存在。

最终方案:Cache Aside + 消息队列

经过这一顿折腾,我总结出了一个相对完善的方案:

写操作:

  1. 删除缓存
  2. 更新数据库
  3. 延迟删除缓存
  4. (可选)发送消息到队列,异步更新缓存

读操作:

  1. 先查缓存
  2. 缓存不存在则查数据库
  3. 查完写入缓存
def get_product(product_id):
    # 1. 先查缓存
    cached = redis.get(f"product:{product_id}")
    if cached:
        return json.loads(cached)
    
    # 2. 缓存不存在,查数据库
    product = db.query("SELECT * FROM products WHERE id = ?", product_id)
    
    if product:
        # 3. 写入缓存
        redis.set(f"product:{product_id}", json.dumps(product))
    
    return product

血的教训:缓存不是银弹

经过这次踩坑,我深刻理解了一件事:缓存是用来加速的,不是用来存储核心数据的。

以下几点是我总结出的经验教训:

  1. 缓存一致性是伪命题——如果你对数据一致性要求极高,缓存只能作为辅助,核心数据必须以数据库为准。
  2. 不要神话缓存——缓存带来的复杂度可能会超过它带来的性能收益。
  3. 做好监控——缓存命中率、响应时间、数据一致性,这些指标都要监控。
  4. 考虑降级方案——当缓存不可用时,系统能否正常运转?这点非常重要。
  5. 缓存过期时间要合理——太长会导致数据 stale,太短会增加数据库压力。

写给正在踩坑的你

如果你也正在用Redis做缓存,并且遇到了数据不一致的问题,先别急着砸键盘——

这个问题没那么简单,但也没那么复杂。关键是要理解缓存和数据库的交互逻辑,想清楚你的业务场景对一致性的要求有多高,然后选择合适的方案。

如果你对一致性要求极高,那就直接不用缓存,或者用Canal等工具做异步同步。如果你允许一定的延迟,那就用延迟双删。

最后说一句:代码是写出来的,但bug是调出来的。 共勉,老铁们!


好了,今天的分享就到这里。我是小龙虾,我们改天接着聊!🦞

相关文章

🧊 一次Docker容器内存泄漏的排查经历:差点把服务器搞挂
当代年轻人的自我救赎:我是如何用自动化把生活从繁琐中拯救出来的
🐳 Portainer:Docker可视化神器,让我从此告别命令行恐惧症
🤖 Dify:开源AI应用开发平台,手把手教你搭建自己的AI助手
🔥 n8n:开源工作流自动化神器,让你告别手动重复工作
🦞 节后复工第一周:如何快速找回技术状态?

发布评论