04 数据同步问题

本文的数据同步问题指的是mysql与redis之间的数据一致性问题。对于主从机器之间的数据同步问题分别查看mysql/redis。

数据一致性问题

MySQL 和 Redis 通常一起使用构成缓存系统(MySQL 作为持久层,Redis 作为缓存层),这就涉及到两者之间数据同步,当遇到网络延迟、服务宕机、并发冲突时就可能导致数据不一致。分析数据一致性问题时可以从【第一步成功、第二步失败】、【并发场景】分析,主要有以下几种情况:
  • 更新数据库成功,但更新缓存失败: 当程序先更新了 MySQL,但由于网络问题或 Redis 宕机,导致删除或更新 Redis 缓存失败,此时数据库是最新数据,而缓存中是旧数据(脏数据)。
  • 并发写入冲突: 发生在更新数据库+更新缓存场景。两个请求同时更新同一条数据,A 先更新数据库,但更新缓存较慢;然后请求 B 更新数据库,并迅速更新缓存。最终,数据库是请求 B 的最新值,而缓存却是请求 A 的旧值。
  • 并发读写冲突:发生在先删除缓存+更新数据库场景。删除缓存后,如果更新 MySQL 还没完成,有其他线程先查询,会到数据库中读取旧数据写入 Redis(缓存回种)。
通常我们重点考虑并发场景下引起的问题,即后两个。

解决方案

实际思考解决方案时要看使用场景,最终一致性和强一致性的解决方案不同。由于强一致性的性能较差 日常使用场景较少,一般都是最终一致性即可。
  • 普通场景:采用“先更新数据库,再删除缓存”的策略,简单有效。
  • 一致性有更高要求:使用消息队列或 Binlog 异步更新缓存。
  • 强一致性:分布式事务
1、最终一致性
综合来看,数据库部分都是更新,缓存部分解决方案可以有 更新缓存、删除缓存 两大类,由于更新缓存的方案在高并发时会有不一致问题,所以普通需求都采用删除缓存的方案。
  • 为什么是‘删除’而不是‘更新’:高并发时,两个线程同时更新数据,A 先更新数据库,但更新缓存较慢;然后请求 B 更新数据库,并迅速更新缓存。最终,数据库是请求 B 的最新值,而缓存却是请求 A 的旧值。如果加分布式锁会对性能有影响,所以采用直接删除缓存的方案。
而删除缓存这类方案按照操作顺序又可以分为【先更新数据库+再删除缓存】、【先删除缓存+再更新数据库】两种。
先更新数据库+再删除缓存(常用)
业界公认的最安全、最主流的更新缓存策略,因为它最大限度地避免了并发场景下的数据不一致问题。又称为旁路缓存(Cache-Aside)模式。
  • 读:先查缓存,缓存没有则去数据库查并写入缓存。
  • 写:先更新数据库,再删除缓存。
这种方案发生数据不一致的概率极低,但是也会存在第二步删除失败的问题,解决方案是删除操作通过「消息队列」或「订阅binlog日志」来实现。
消息队列:在业务代码中,完成数据库更新后,删除缓存的操作发送到消息队列中,由独立的消费者去删除。这种方式将缓存操作从主业务流程中解耦,保证了业务的性能,并且通过消息队列的重试机制,确保了最终一致性。
订阅binlog日志: 这种方案对业务代码完全无侵入。它通过一个工具(如 Canal)模拟成 MySQL 的从库,实时监听数据库的 Binlog。当数据库发生变更时,工具会自动捕获并通知一个消费者服务去删除 Redis 缓存。这是目前最可靠、对业务影响最小的解决方案之一。
先删除缓存+再更新数据库
也是一种更新缓存策略,但是在并发读写冲突时会导致缓存回种,可以结合「延迟双删」来解决。
延迟双删:先删除缓存并更新数据库,休眠一小段时间后再次删除缓存,第二次删除能够把回种的数据清理掉。但是延迟时间设置多久很难评估,通常根据经验可以设置1~5s,而极端情况还是会有数据不一致。所以这种方案仅作为备选。
2、强一致性
MySQL 与 Redis 更新要么一起成功,要么一起失败。但是方案复杂且性能差,而引入缓存的目的就是提升性能,所以应尽量避免使用。
  • 方案:分布式事务(2PC/TCC),比如用 RocketMQ 事务消息,在 MySQL 事务提交后再更新 Redis。
  • 优点:数据绝对一致。
  • 缺点:方案复杂、性能差。
参考资料:

 

分界线

 

mysql读写分离(主从同步)

  • 在单台mysql实例的情况下,所有的读写操作都集中在这一个实例上。

  • 当读压力太大,单台mysql实例扛不住时,此时DBA一般会将数据库配置成集群,一个master(主库),多个slave(从库),master将数据通过binlog的方式同步给slave,可以将slave节点的数据理解为master节点数据的全量备份。

如果是写操作(insert、update、delete等),就走主库,主库会将数据同步给从库;读操作(select、show、explain等),就走从库,从多个slave中选择一个,查询。

  • 读写分离优点

    • 避免单点故障。

    • 负载均衡,读能力水平扩展。通过配置多个slave节点,可以有效的避免过大的访问量对单个库造成的压力。

  • 读写分离挑战

    • 对sql类型进行判断。如果是select等读请求,就走从库,如果是insert、update、delete等写请求,就走主库。

    • 主从数据同步延迟问题。因为数据是从master节点通过网络同步给多个slave节点,因此必然存在延迟。因此有可能出现我们在master节点中已经插入了数据,但是从slave节点却读取不到的问题。对于一些强一致性的业务场景,要求插入后必须能读取到,因此对于这种情况,我们需要提供一种方式,让读请求也可以走主库,而主库上的数据必然是最新的。

    • 事务问题。如果一个事务中同时包含了读请求(如select)和写请求(如insert),如果读请求走从库,写请求走主库,由于跨了多个库,那么jdbc本地事务已经无法控制,属于分布式事务的范畴。而分布式事务非常复杂且效率较低。因此对于读写分离,目前主流的做法是,事务中的所有sql统一都走主库,由于只涉及到一个库,jdbc本地事务就可以搞定。

    • 高可用问题。主要包括:

      • 新增slave节点:如果新增slave节点,应用应该感知到,可以将读请求转发到新的slave节点上。

      • slave宕机或下线:如果其中某个slave节点挂了/或者下线了,应该对其进行隔离,那么之后的读请求,应用将其转发到正常工作的slave节点上。

      • master宕机:需要进行主从切换,将其中某个slave提升为master,应用之后将写操作转到新的master节点上。

posted @ 2024-08-13 01:14  zhegeMaw  阅读(16)  评论(0)    收藏  举报