更新数据库后直接更新缓存方案
直接更新缓存的核心优势
减少缓存穿透风险:直接设置缓存可以避免缓存删除后短期内的缓存穿透问题(即大量请求穿透到数据库)。尤其在高频更新场景下,连续写入时可减少缓存未命中的概率。
数据一致性优先:直接获取最新的数据库记录后立即更新缓存,确保缓存数据与数据库强一致,避免传统"删除缓存+后续查询重建"模式可能产生的短暂数据不一致窗口。
理论上直接更新缓存是可以带来上述的这些有点。尤其第二点理论上在没有并发写的场景下可以实现实时的数据一致性。
潜在的风险:
更新完数据,查询数据,更新缓存。由于主从复制导致的延迟,实际上有可能导致虽然落库成功了,但是从库依然是旧数据,导致查询到的依然是旧数据,导致缓存中的脏数据。
并发一致性问题:
假设两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?
有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:
- 线程 A 更新数据库(X = 1)
- 线程 B 更新数据库(X = 2)
- 线程 B 更新缓存(X = 2)
- 线程 A 更新缓存(X = 1)
最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。
也就是说,A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。
缓存更新失败的场景。若数据库更新成功但缓存设置失败(如网络问题),会导致 缓存与数据库数据不一致 。没有引入重试机制。
解决方案
Cache-Aside模式
基于上述的问题点,我们可以采用如下的一些策略解决。
采用Cache-Aside模式即更新数据库操作之后,删除缓存。
缓存穿透解决方案
这种方案在查询更新缓存时,可能会有短暂的缓存穿透问题,该问题可以通过使用SingleFlight(即允许具有相同键的并发调用共享调用结果)解决。
数据一致性
另外这种方案导致的数据一致性问题的概率极低。
依旧是 2 个线程并发「读写」数据:
- 缓存中 X 不存在(数据库 X = 1)
- 线程 A 读取数据库,得到旧值(X = 1)
- 线程 B 更新数据库(X = 2)
- 线程 B 删除缓存
- 线程 A 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。
这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?
其实概率「很低」,这是因为它必须满足 3 个条件:
- 缓存刚好已失效
- 读请求 + 写请求并发
- 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间短(步骤 2 和 5)
仔细想一下,条件 3 发生的概率其实是非常低的。
因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。
这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
所以,我们应该采用这种方案,来操作数据库和缓存。
主从延迟问题应对
这个方案也有可能因为主从同步导致的缓存脏数据问题。
- 延迟双删策略:
- 更新数据库后先删除缓存,业务逻辑处理完成后延迟一段时间(如 500ms)再次删除缓存,确保主从同步完成。
- 数据库日志订阅:
- 通过监听 Binlog 实时捕获数据变更,异步更新缓存,消除主从延迟影响。
总结
封装缓存组件,缓存操作流程对开发者不可见,通过函数的方式传参,仅把数据库持久化操作的方法暴露给开发者。我在这篇文章《基于约束大于规范的想法,封装缓存组件》中写过一个解决方案。