知识卡片

延时消息队列解决缓存穿透脏数据

结构图卡

内容

[[异地多活的五大挑战]]中,微博对数据同步问题的解法是:每个机房的缓存完全独立,由每个机房专门负责消息处理的Processor(类似Storm)根据收到的WMB消息更新本机房缓存;由于消息不重复分发、且信息完备,[[微博异地多活的技术选型演进:从MytriggerQ失败到MCQ的WMB方案]]中MytriggerQ方案存在的缓存脏数据问题就此解决了。但这套方案本身还留了一个缝隙:当缓存不存在时,请求会穿透到MySQL从库、再回种到缓存——如果这时候从库正好有延迟(主从同步还没跟上),穿透读到的就是旧数据,这份旧数据会被错误地种进缓存,造成新的脏数据。微博的解决方案是专门设计一个延时10分钟的消息队列,配一个处理程序根据这个延时队列的消息重新加载数据——因为从库延迟一般不超过10分钟,而在微博的业务场景下,10分钟内出现的这点脏数据本身也是可以接受的。这个设计的巧妙之处在于:它没有试图从根本上消除”缓存穿透时从库可能延迟”这个问题(这个问题本质上无法被彻底消除,除非做强一致的同步复制,但那会带来更大的性能和可用性代价),而是承认这类脏数据短暂存在是不可避免的,转而用一个”延时重新加载”的补偿机制去兜底——用一次延时的、批量的、代价很低的”纠正”,换掉了原本要为消除脏数据付出的实时强一致性代价。对于数据库同步本身,微博选的也是同一种”接受有限风险、换取简单可靠”的策略——因为对数据库不是强依赖、加上数据库双写的维护成本过大,选择的是主从同步方式,这套方案跑了3年整体非常稳定,没有发生过因数据同步导致的服务故障。

结构图

flowchart TD
    A["请求命中缓存未命中"] --> B["穿透读MySQL从库"]
    B --> C{"从库是否有延迟?"}
    C -->|否| D["读到最新数据,回种缓存"]
    C -->|是,读到旧数据| E["旧数据被错误回种缓存\n(脏数据)"]
    E --> F["延时10分钟的消息队列\n触发处理程序重新加载"]
    F --> G["10分钟内脏数据自动纠正\n(业务可接受该窗口)"]

参考来源

- 位置:《高可用架构(第1卷)》第1章《高可用架构案例精选》"1.7 微博"异地多活"部署经验谈"节,"1.7.2 微博异地多活面临的挑战"(源文件:_epub-src/OEBPS/Text/Chapter1_7_3.xhtml) - 结论依据:原文说明"可能出现的问题是,缓存穿透,但是MySQL从库如果此时出现延迟,这样就会把脏数据种到缓存中。我们的解决方案是做一个延时10分钟的消息队列,然后由一个处理程序来根据这个消息做数据的重新载入……10分钟内的脏数据在微博的业务场景下也是可以接受的",直接支撑本卡片结论与结构图。 - 原始内容:可能出现的问题是,缓存穿透,但是MySQL从库如果此时出现延迟,这样就会把脏数据种到缓存中。我们的解决方案是做一个延时10分钟的消息队列,然后由一个处理程序来根据这个消息做数据的重新载入。