写点什么

对线面试官 -Redis 十一 | 双写一致性问题

作者:派大星
  • 2023-07-20
    辽宁
  • 本文字数:2820 字

    阅读完需:约 9 分钟

Redis 双写一致性问题解决方案的终结篇


在之前的文章中有介绍过关于缓存一致性的问题,那么为什么还要出一篇文章来再次说明呢?是因为之前的文章主要讲述了高并发架构下缓存一致性问题可以通``延时双删进行解决,高可用架构(读写分离)采用的是先更新数据库,然后再删除缓存,并最后采用重试机制进行避免。之前文章的高可用解决方案后续优化的点侧重于结合binglog的方式去解决。这篇文章又结合了 JVM 队列的方式。具体有细节可以阅读本篇文章。相信对你在实际应用中会有很大帮助,至于实际应用采用什么方式。还需要结合实际的业务场景。


这片文章更侧重于高可用架构也就是读写分离架构的解决方案是如何实施的。


面试官:在实际的工作中,你们 Redis 是如何保证缓存与数据库的双写一致性呢?


面试官心里分析:主要考察实际工作中到底是使用没使用过 Redis,因为使用过 Redis 的话一定会遇到双写一致性的问题。那么如何解决的呢?需要简单描述一下当时的方案,这样才能确保你确实是使用过而不是简单的背背八股文,没有实战经验的小白。


派大星:对于这个问题其实在实际应用中,Redis 最经典的方式就是Cache Aside Pattern,也就是缓存+数据库读写模式。


  • 读的时候,先读缓存缓存没有的话,就读数据库然后把数据库的数据再次放入到缓存中。同时返回响应。

  • 更新的时候,先更新数据库,然后再删除缓存


面试官:那为什么是先更新数据库,而不是先更新缓存/删除缓存呢?


派大星:其实这样操作的原因很简单,在实际的业务场景中,缓存应用是有相对比较复杂的情况的,因为缓存有可能不单单是数据库中取出来的值,而是通过查询数据库某些值然后运算后才放入的缓存中,这样来说如果不是热点数据的话,每次更新数据库都去更新缓存,这样其实对于这种比较复杂的缓存数据的情景下更新缓存的代价还是比较高的。还有另外一点就是为什么删除缓存而不是更新缓存?这里其实就是一个lazy计算的思想。不管它是否用到每次都去重新做复杂的计算,建议采用当其被需要使用的时候再去重新计算。想MyBatis``Hibenate他们都是有懒加载的思想的。


派大星:其次其实为什么是先更新数据库,而不是先更新缓存/删除缓存的原因也很简单。这种方式只能是解决掉简单的缓存架构(高并发架构)的双写一致性的问题(当然这种解决法方式在高并发的情况下也是有线程安全问题,真正的解决方案是延时双删) 。具体思路在之前的文章有提到过:


面试官:ok,那你具体说说双写一致性的问题到底应该如何解决?即具体的实施方案。


派大星:好的,其实采用之前文章的延时双删的方案在流量并不是很多的情况下已经可以解决。但是如果是亿万级流量或者流量真的很高的情况下。采用那种方案是远远不够的。具体如何解决其实可以参考一下之前的文章:这一次提供一个不一样的解决方案:


派大星:首先更新数据的时候,可以根据数据的唯一标识,将操作路由之后,发送到一个 JVM 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么此时应该采用重新读区数据+更新缓存的操作,根据唯一标识路由之后,也发送到同一个 JVM 队列中。一个队列对应一个工作线程,每个工作线程串行拿到对应的操作之后一条一条的执行。这样操作,一个数据的变更就会按章先删除缓存然后再去更新数据库,然后在更新缓存。即使在更新数据库没完成的时候来了一个请求没有读到缓存,那么也可以将它的更新缓存的操作发送到队列中,此时 JVM 队列就会有积压,直到同步等待缓存更新完成。


当然这样操作也是有一个优化点的:一个队列中,其实多个更新缓存请求串联在一起是没有意义的,因此我们可以在这里做一些过滤。如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新缓存的请求进去了。只需要等待前面的缓存更新请求完成即可。等待那个对应的队列的工作线程完成了上一个操作的数据的修改之后,才会执行下一个操作。也就是更新缓存的操作。此时把数据库中最新的值读取出来写入缓存即可。当然如果请求在等待的时间范围内,我们可以不断的轮询直到取到值返回即可,但是如果等待时间超过了一定时长我们可以直接将数据库中的旧值读取并返回。


面试官:这样设计的话在高并发场景下不会有问题吗?


派大星:是会有一些问题的。比如:


  • 读请求长时间阻塞


读请求长时间阻塞。由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题。每个读请求一定要控制在超时时间范围内返回。其次的问题就是该解决方案,最大的风险点在于:可能数据更新很频繁,导致队列中积压了大量的更新操作在里面,然后读请求会发生大量的超时,最后导致大量的读请求直接请求到数据库上。针对这种情况需要针对真实的业务场景模拟真实的测试环境,来查看具体的更新频率如何。还有一点就是,因为在一个队列中,可能会积压针对多个数据项的更新操作,因此这就需要根据实际业务场景来进行测试。比如可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个队列中积压了 100 个商品的库存修改操作,每个库存修改操作耗费 10ms,那么最后一个商品的去请求,可能会等待 10*100=100ms=1s 后,才能得到这个数据,这个时候就导致了去请求的长时间阻塞(这里一定要注意模拟实际生产环境到底可能会积压多少个更新请求在队列中,越多 hang 的时间则就越长)。如果真的是一个内存队列中积压的更新操作特别多,此时就需要考虑加机器了让每个机器部署的服务实力处理更少的请求。这样每个内存队列中积压的更新操作才会更少。


  • 读请求并发量过高。


另外一种风险就是:读请求并发量过高。就是突然之间大量读请求会在短时间内(比如几十毫秒)的延时 hang 在服务上。看看服务能不能扛得住。需要多少机器才能抗住最大的极限情况的峰值。但是因为并不是所有的数据都在同一时间段内更新,缓存也不会在同一时间内失效,所以每次可坑就是更新少数的缓存。并发量不会特别大。


  • 多服务实例部署的请求路由


如果说你的服务部署了多个实例,那么必须要保证的是,执行数据更新操作,以及执行缓存的更新操作的请求都需要通过 Nginx 服务器路由到相同的服务实例上


  • 热点商品的路由问题,导致请求的倾斜


比较特殊的情况是,万一某个商品的读写请求都是特别高,全部打到相同的机器和相同的队列里面去了,可能会造成某台机器的负载压力过大。因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发。所以其实要根据实际业务系统查看,如果更新频率不是很高的话这个问题就不是什么大的问题。但还是会有可能造成某些机器的负载压力高一些。


面试官:不错不错。看来实际中确实应用场景比较丰富。


派大星:是的呢。就是简单概括来说:如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。


如有问题,欢迎加微信交流:w714771310,备注-技术交流 或关注微信公众号【码上遇见你】。

发布于: 刚刚阅读数: 4
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
对线面试官-Redis 十一 | 双写一致性问题_Java 面试题_派大星_InfoQ写作社区