那我们有没有什么手段来减少这种不一致的情况出现呢?这时候借助一个可靠的消息中间件就是一个不错的选择。
因为消息中间件有 ATLEAST-ONCE 的机制,如下图所示。
我们把删除 Redis 的请求以消费 MQ 消息的手段去失效对应的 Key 值,如果 Redis 真的存在异常导致无法删除成功,我们依旧可以依靠 MQ 的重试机制来让最终 Redis 对应的 Key 失效。
而你们或许会问,极端场景下,是否存在更新数据库后 MQ 消息没发送成功,或者没机会发送出去机器就重启的情况?
这个场景的确比较麻烦,如果 MQ 使用的是 RocketMQ,我们可以借助 RocketMQ 的事务消息,来让删除缓存的消息最终一定发送出去。而如果你没有使用 RocketMQ,或者你使用的消息中间件并没有事务消息的特性,则可以采取消息表的方式让更新数据库和发送消息一起成功。事实上这个话题比较大了,我们不在这里展开。
有些时候,真实的缓存场景并不是数据库中的一个记录对应一个 Key 这么简单,有可能一个数据库记录的更新会牵扯到多个 Key 的更新。还有另外一个场景是,更新不同的数据库的记录时可能需要更新同一个 Key 值,这常见于一些 App 首页数据的缓存。
我们以一个数据库记录对应多个 Key 的场景来举例。
假如系统设计上我们缓存了一个粉丝的主页信息、主播打赏榜 TOP10 的粉丝、单日 TOP 100 的粉丝等多个信息。如果这个粉丝注销了,或者这个粉丝触发了打赏的行为,上面多个 Key 可能都需要更新。只是一个打赏的记录,你可能就要做:
updateMySQL();//更新数据库一条记录
deleteRedisKey1();//失效主页信息的缓存
updateRedisKey2();//更新打赏榜TOP10
deleteRedisKey3();//更新单日打赏榜TOP100
这就涉及多个 Redis 的操作,每一步都可能失败,影响到后面的更新。甚至从系统设计上,更新数据库可能是单独的一个服务,而这几个不同的 Key 的缓存维护却在不同的 3 个微服务中,这就大大增加了系统的复杂度和提高了缓存操作失败的可能性。最可怕的是,操作更新记录的地方很大概率不只在一个业务逻辑中,而是散发在系统各个零散的位置。
针对这个场景,解决方案和上文提到的保证最终一致性的操作一样,就是把更新缓存的操作以 MQ 消息的方式发送出去,由不同的系统或者专门的一个系统进行订阅,而做聚合的操作。如下图:
不同业务系统订阅 MQ 消息单独维护各自的缓存 Key
专门更新缓存的服务订阅 MQ 消息维护所有相关 Key 的缓存操作