我们在 Mysql 存储集群架构中, 经常采用一主多从模式部署。主节点提供写的能力, 从节点提供读的能力, 有效分担了主单点的压力.
然而这个世界本身就不是完美的, 当选择一个方案的时候总是会附带着各种问题来折磨你.
我们都知道 Mysql 数据库同步机制是依靠主库的 binlog 异步复制给其他从库, 核心的问题就出现在这个异步上, 常见的逻辑是我们会写入主库成功后再删除掉缓存, 后续业务读请求发现无缓存后, 重新读取从库数据被动式构建缓存, 但是从库还未更新完成, 导致了缓存还是旧数据.
这篇文章我们主要探讨此类问题的常见解决方式.
如图所示: 业务写入主库, 主库同步给从库, 业务读取从库数据同步给redis.
image.png
问题在于主从数据同步管道延迟是不可知的, 当网络抖动或写入业务高峰期时, 很容易出现数据同步管道延迟, 导致缓存不是最新的数据.
导致经常与DBA扯皮, 并且这是一个非常说不清楚的事情, 因为导致这个延迟的因素太多了, 根据墨菲定律, 业务方一定要在在开发之前就要思考如何避免出现这种问题, 在做技术方案要给各方说清楚这件事, 了解业务核心诉求.
抛弃主从结构, 读写都切换为主库, 这样是可以避免写入的缓存可能不一致的问题。对于小型业务来说是没有问题, 但是如果查询量较大情况下, 主节点压力过大, 一般不建议采用此种方案.
image.png
我们可以按照业务场景区分, 对于及时性比较高的场景 (比如此文举例的缓存构造场景) 去读取主库, 对于其他实时性要求不是非常高的场景, 我们可以采用读取从库的方式. 这里需要认真思考下业务场景
建议临时解决可以使用此方案, 但是需要全面评估使用, 很容易被漏用或滥用.
image.png
延迟双删指的是先删除缓存, 间隔指定时间后再次尝试进行删除缓存, 防止出现缓存污染的数据. 这里我们假定了在延迟时间内构造的缓存都视为脏数据, 进行再次删除操作双保险.
这种方案问题在于在延迟时间内是可能存在不一致情况的, 并且具体最大延迟时间去删除缓存很难去评估.
image.png
这种方案本质上也是延迟双删, 写入数据库成功后删除缓存, 只不过我们将固定的延迟时间触发删除缓存改为通过binlog通知删除缓存.
这种方案问题也在于延迟时间内存在不一致的情况, 即使收到 binlog event 通知后也不一定会通知完所有从库, 同样存在不一致的风险, 但相比指定时间方案来说, 这种方案最大的优势是可以根据系统的实际情况进行删除缓存。
实现复杂度略高, 需要额外维护一套系统.
image.png
以上我们说的都是被动式缓存, 被动式指的是我们发生写操作只会删除缓存, 如果缓存不存在则会读取数据库, 将结果往 redis 里设置缓存, 所有缓存生成是由具体业务根据请求时来生成的.
这种方案有一个优点就是心智负担低, 基本不需要考虑缓存的生命周期, 对于缓存集群架构升级也是非常友好的. 但是简单带来的影响是 cache miss 率是非常高的, 所以在一些大型项目初始化中会有一个预热的过程.
被动式缓存对应的是主动式缓存, 指的是当数据发生编辑、新增时, 主动构建缓存, 这种方案可以最大避免因为主从延迟造成的问题, 缺点就是缓存管理成本高, 缓存节点变为有状态节点, 架构不灵活, 缓存污染发现不及时(需要搭配额外任务进行数据比对)
image.png
其他一些方案:
2. 只写缓存 -> 异步同步数据库
对于一些不是非常重要的信息比如点赞数, 我们可以先只更新缓存, 以固定频率或数值大小进行数据库同步. 这种方式直接避免了读从库的不一致, 非常有效降低数据库的压力, 但是对于数据是存在丢失风险的.
image.png
一般有过期时间的主动式缓存 + 被动式缓存搭配使用也是一个很好的方案, 兼容了缓存的正确性以及灵活性. 虽然不能完全能够解决掉一致性问题, 但可以有效缩短不一致时间和机率.
当然还有很多方案, 所有的方案落地还需要结合实际业务诉求, 区分出属于对延迟敏感的数据和非敏感数据, 例如商品的简介可能对缓存一致性要求不高, 可以直接读取缓存, 价格属于敏感信息选择直接读取主库等, 归根到底还是要看实际的业务场景.
你有什么更好的想法呢? 欢迎评论区讨论哈.