常见的有三种方法:
- 先更新数据库,再删除缓存 (适用于绝大数业务场景,但是不能保证最终数据一致性,见2)
- 采用「先更新数据库,再删除缓存」方案,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据最终一致,(但是如果更新数据库时,缓存中还没删除,此时读缓存还是读的脏数据)
- 「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率(延时时间比较难确定)
说明:如果你想要最终一致性可以使用 Binlog 异步更新缓存方案,如果缓存实时性要求比较高可以使用先更新数据库再删缓存方案
先更新数据库,再删除缓存(配合消息队列或canal+MQ):
总结:采用「先更新数据库,再删除缓存」方案,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志+MQ」的方案来做,本质是通过「重试」的方式保证数据最终一致
更新数据库这一步不容易出现更新失败的情况,因为数据库事务有持久性(redo日志可以保证掉电等故障恢复)
但是删除缓存容易出现问题(虽然有持久化…),容易出现断电操作失败情况,解决方式是“重试”。
方式一(消息队列):直接把删除缓存这一步,直接放到消息队列中,由消费者来操作缓存(消息队列是可以保证消息一定被消费的,不用担心消息消费失败问题),这时架构模型就变成了这样:
方式二(订阅数据库变更日志Binlog+MQ):订阅数据库变更日志,再操作缓存。具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。那什么时候操作缓存呢?这就和数据库的「变更日志」有关了。拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,通过消息队列去删除对应的缓存。
订阅变更日志目前有成熟的中间键,如阿里的canal,配合消息队列无需考虑消息消费失败的情况。(Canal 本身不包含消息队列,但它原生支持与消息队列集成。)
具体来说:
- 数据库发生写操作的时候会将这些记录写入binlog中,
- 使用 Canal 监听和解析Binlog中的变更记录
- Canal 会将数据库的变更内容转化为相应的消息,并将消息推送到指定的 RocketMQ Topic,
- 消费者就可以完成缓存删除/更新操作。
因此,在 Spring Boot 应用中,我们只需要完成canal与 RocketMQ 进行对接即可。
延迟双删:
更新数据库之前,先删除缓存再更新数据库,更新完成,再过一段时间再删除缓存。但是这个延迟时间很难评估。
还是用第一个方法比较好