作者:微信小助手
发布时间:2024-11-08T00:34:59
对于读多写少并且要求高性能的业务逻辑,我们通常在应用服务器访问MySQL数据库的中间加上一层Redis缓存层,以提高数据的查询效率,减轻MySQL数据库的压力,避免在MySQL出现性能瓶颈。 该问题,如果在数据存储后,只读场景下是不会出现MySQL与Redis缓存的一致性问题的,所以真正需要考虑的是并发读写场景下的数据一致性问题。 如果我们不加分析,单独利用MySQL和Redis的知识进行回答并发场景下如何保证MySQL与Redis缓存一致性?很难把这个问题回答好,因为看起来很简单的方案实际上是漏洞百出的。 我们先看下简单的更新数据库、删除缓存和更新缓存方案下,会出现什么问题? 先说结论: 原因是更新缓存成功后,数据库可能更新失败,出现数据库为旧值,缓存为新值。导致后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持了数据的完全不一致!并且当前数据库中的值为旧值,而业务数据的正确性应该以数据库的为准。 那么如果更新缓存成功后,数据库可能更新失败,我们重新更新缓存是不是可以了? 抛开需要重新更新缓存时,要单表或多表重新查询数据,再更新数据带来的性能问题,还可能期间有数据变更再次陷入脏数据的情况。实际上仍然还是会出现并发一致性问题。 只要缓存进行了更新,后续的读请求在更新数据库前、更新数据库失败并准备更新缓存前,基本上都能命中缓存情况,而这时返回的数据都是未落库的脏数据。 不考虑。 原因是当数据库更新成功后,缓存更新失败,出现数据库为最新值,缓存为旧值。导致后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持了数据的完全不一致! 该方案就算在更新数据库、更新缓存都成功的情况下,还是会存在并发引发的一致性问题,如下图所示(点击图片查看大图): 可以看到在并发多写多读的场景下数据存在的不一致性问题。 不考虑,但是通过使用延时双删策略后可以考虑。 采用“先删除缓存,再更新数据库”的方案是一种常见的方法来尝试解决这个问题的策略。 这种方法逻辑较为简单,易于理解和实现,理论上删除旧缓存后,下次读取时将从数据库获取最新数据。 但在并发的极端情况下,删除缓存成功后,如果再有大量的并发请求进来,那么便会直接请求到数据库中,对数据库造成巨大的压力。而且此方案还是可能会发生数据不一致性问题。 通过上图发现在删除缓存后,如果有并发读请求 对此我们可以先进行一波的小优化,那就是延时双删策略。即在更新数据库之后,先延迟等待一下(等待时间参考该读请求的响应时间+几十毫秒),再继续删除缓存。这样做的目的是确保读请求结束(已经在 可以看出此优化方案关键点在于等待多长时间后,再次删除缓存尤为重要,但是这个时间都是根据历史查询请求的响应时间判断的,实际情况会有浮动。这也导致如果等待的延时时间过短,则仍然会出现数据不一致的情况;等待延迟时间过长,则导致延迟期间出现数据不一致的时间变长。 另外延时双删策略还需要考虑如果再次删除缓存失败的情况如何处理? 因为删除失败将导致后续的所有的读请求,在缓存未过期或缓存未重新正确更新的情况下,会一直保持了数据的完全不一致!这个在下文的技术优化方案继续讨论。 比较推荐。 采用的“先更新数据库,再删除缓存”策略,跟“先删除缓存,再更新数据库”中我们进行延时双删策略的小优化基本一样,仍然需要考虑删除缓存失败的情况如何处理。 单纯从“先更新数据库,再删除缓存”和“先删除缓存,再更新数据库”对比起来。在大多数情况下,“先更新数据库,再删除缓存”被认为是一个更好的选择,原因如下: 但该方案同样也会出现数据不一致性问题,如下图所示。 当数据库的数据被更新后,缓存也被删除。接下来的出现 读请求先读了缓存发现缓存无命中,则查询数据库并在准备更新缓存时,3.2写请求已经完成了数据的更新和删除缓存的动作,之后3.1读请求才更新了缓存。最后导致了数据库中的值未新值,缓存中的值为旧值。 从上面的简单方案方案中,似乎没有一种方案真正能解决并发场景下MySQL数据与Redis缓存数据一致性的问题。 这里有个说明下,如果业务要求必须要满足强一致性,那么不管如何优化缓存策略,都无法满足,而最好的办法是不用缓存。 强一致性:它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。 解决方案是读写串行化,而此方案会大大增加系统的处理效率,吞吐量也会大大降低。 另外在大型分布式系统中,其实分布式事务大多数情况都不会使用,因为维护成本太高了、复杂度也高。所以在分布式系统,我们一般都会推崇最终一致性,即这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。 现在我们接着继续优化.. 从上面 但我们前面还遗留了一个待解决的问题:如果再次删除缓存失败的情况如何处理? -----当然是补救去继续删除这个缓存Key了,而补救方法则是重试。 重试机制可以在当前中启动新协程(Golang中属于用户态的轻量级线程)中进行重试;也可以放到消息队列中进行重试;还可以是先启动新协程重试3次,重试失败后继续放到消息队列中重试,如下图展示的是放到消息队列中进行重试。 新协程中进行重试需要注意的是使用的新上下文context.Background(),而不是当前请求的上下文。 一般消息队列会支持高可靠性的队列,例如 RabbitMQ、Kafka 等。这些消息队列提供了非常强的消息传递、异步处理和持久化功能,可以有效地解决数据同步的问题。 此方案仍然存在一些需要,如:选择合适的延迟等待时间进行删除缓存;协程中重试删除缓存次数、间隔时间;消息队列中删除失败缓存失败后是否需要重试等。 重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。 其实,还可以这样优化: 异步淘汰key相比于等新对比缓存数据并更新会简单一些,因为可能一份缓存数据涉及多张表的数据查询、聚合、排序等。 尽管该方案看起来也不错了,但是因为引入额外的组件(如Canal、消息队列)复杂性增加了也不少,需要维护和监控这些组件的运行状态,保证组件运行正常。 在某些业务场景的需求下,也可以通过定时任务的方式进行 Redis 和 MySQL 的数据同步。 具体做法是通过定时任务从 Redis 中读取数据,然后跟 MySQL 中的数据进行比对,如果 Redis 中数据有变化,则进行同步。 这种方式虽然实现起来比较简单,但需要注意同步的时效性,如果时间间隔设置不当,可能会导致同步的数据丢失或者不准确。 在更新数据库的同时也更新缓存/删除缓存,即所谓的“双写”。 这样可以确保在数据库更新后,缓存中的数据也是最新的,从而减少数据不一致的时间窗口。 并发控制:在高并发场景下,多个请求同时对同一个数据进行更新时,如果没有妥善处理并发控制,可能会导致数据不一致的问题。所以这里引入了分布式锁和事务操作: 当然在“双写”的策略中,除了并发控制外,可以结合上面提到的重试、定时策略进行组合,以应对极端情况下的数据不一致性问题。 另外也可以处理失败的逻辑上加入告警机制,及时通知开发和运维人员。简单方案下的漏洞百出
更新缓存,再更新数据库
不考虑
。更新数据库,再更新缓存
先删除缓存,再更新数据库
1.1
进来,那么查询缓存肯定是不存在,则去读取数据库,但因为此时更新数据库x=10的操作2.更新数据库
还未完成,所以读取到的仍然是旧值x=5并设置缓存后,在2.更新数据库
完成后,数据是新值10,而缓存是旧值,造成了数据不一致的问题。1.2读库
中读取到了旧数据,后续会在该请求中更新缓存),写请求可以删除读请求造成的缓存脏数据,保证再删除缓存之后的所有读请求都能读到最新值。先更新数据库,再删除缓存
读请求3.1
和写请求3.2
同时进来。优化后方案
延迟双删策略+重试机制
简单方案下的漏洞百出
下的先删除缓存,再更新数据库
中,我们可以看出来其实延迟双删策略,算是融合“先删除缓存,再更新数据库”和“先更新数据库,再删除缓存”的策略,可以解决大部分的数据一致性的业务逻辑处理问题。
读取binlog异步删除缓存
定时任务
双写一致性