先删除缓存,再更新数据库
该方案在线程A进行数据更新操作,线程B进行查询操作时,有可能出现下面的情况导致数据不一致:
- 线程A删除缓存
- 线程B查询数据,发现缓存数据不存在
- 线程B查询数据库,得到旧值,写入缓存
- 线程A将新值更新到数据库
这样一来,缓存中的数据仍然是旧值
如果线程B执行的是更新操作,线程B查询得到的是旧值,A更新到数据库新值,然后B基于旧值计算写入了计算后的值,A的更新操作被抹去了,这种情况下属于更新数据事务原子性问题,需要用分布式锁来解决。
先更新数据库,再删除缓存
当缓存失效时,线程B原子性被破坏时会出现不一致问题:
- 缓存失效了
- 线程B从数据库读取旧值
- 线程A从数据库读取旧值
- 线程B将新值更新到数据库
- 线程B删除缓存
- 线程A将旧值写入缓存
这种情况概率很低,实际上数据库的写操作会比读操作慢得多,读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,这种情况下只需要线程B延时删除缓存就好。另外在数据库主从同步的情况下,延时删除还能防止数据更新还未从主数据库同步到从数据库的情况。
延时双删
延时双删即先删除缓存,然后更新数据,再延时n ms后删除缓存,这个我认为作用和更新数据库再删除缓存的策略几乎是等同的(欢迎讨论)
之所以设计为延时双删的目的在于当最后一次延时删除缓存失败的情况发生,至少一致性策略只会退化成先删缓存再更新数据的策略。
删除缓存失败这种事情个人认为在生产环境缓存高可用的情况下几乎不会出现,且这种情况如果发生了,不如考虑一下重试机制。
异步更新缓存(基于订阅binlog的同步机制)
通过异步更新缓存将缓存与数据库的一致性同步从业务中独立出来统一处理,保证数据一致性
整体技术思路:
- 读Redis:热数据基本都在Redis
- 写MySQL:增删改都是操作MySQL
- 更新Redis数据:订阅MySQ的数据操作记录binlog,来更新到Redis
数据操作分为两大部分:
- 全量更新(将全部数据一次性写入redis)
- 增量更新(实时更新)
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息通过消息队列推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
这种同步机制类似于MySQL的主从备份机制,可以结合使用阿里的canal对MySQL的binlog进行订阅。
总结
综上所述,异步更新缓存、更新后延迟删与延迟双删都是不错的一致性解决方案,但除了异步更新缓存,其余两个方案都会对业务线的代码有侵入性。