乐趣区

关于缓存:如何保证缓存与数据库的双写一致性

作者:你是我的海啸
起源:https://blog.csdn.net/chang38…

分布式缓存是当初很多分布式应用中必不可少的组件,然而用到了分布式缓存,就可能会波及到缓存与数据库双存储双写,你只有是双写,就肯定会有数据一致性的问题,那么你如何解决一致性问题?

Cache Aside Pattern

最经典的缓存 + 数据库读写的模式,就是 Cache Aside Pattern。
读的时候,先读缓存,缓存没有的话,就读数据库,而后取出数据后放入缓存,同时返回响应。

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

为什么是删除缓存,而不是更新缓存?

起因很简略,很多时候,在简单点的缓存场景,缓存不单单是数据库中间接取出来的值。

比方可能更新了某个表的一个字段,而后其对应的缓存,是须要查问另外两个表的数据并进行运算,能力计算出缓存最新的值的。

另外更新缓存的代价有时候是很高的。是不是说,每次批改数据库的时候,都肯定要将其对应的缓存更新一份?兴许有的场景是这样,然而对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁批改一个缓存波及的多个表,缓存也频繁更新。然而问题在于,这个缓存到底会不会被频繁拜访到?

举个栗子,一个缓存波及的表的字段,在 1 分钟内就批改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;然而这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度降低,用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思维,不要每次都从新做简单的计算,不论它会不会用到,而是让它到须要被应用的时候再从新计算。像 mybatis,hibernate,都有懒加载思维。查问一个部门,部门带了一个员工的 list,没有必要说每次查问部门,都外面的 1000 个员工的数据也同时查出来啊。80% 的状况,查这个部门,就只是要拜访这个部门的信息就能够了。先查部门,同时要拜访外面的员工,那么这个时候只有在你要拜访外面的员工的时候,才会去数据库外面查问 1000 个员工。

最高级的缓存不统一问题及解决方案

问题:先批改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就呈现了不统一。

解决思路:先删除缓存,再批改数据库。如果数据库批改失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不统一。因为读的时候缓存没有,则读数据库中旧数据,而后更新到缓存中。

比较复杂的数据不统一问题剖析

数据产生了变更,先删除了缓存,而后要去批改数据库,此时还没批改。一个申请过去,去读缓存,发现缓存空了,去查询数据库,查到了批改前的旧数据,放到了缓存中。随后数据变更的程序实现了数据库的批改。

完了,数据库和缓存中的数据不一样了。。。

为什么上亿流量高并发场景下,缓存会呈现这个问题?

只有在对一个数据在并发的进行读写的时候,才可能会呈现这种问题。其实如果说你的并发量很低的话,特地是读并发很低,每天访问量就 1 万次,那么很少的状况下,会呈现方才形容的那种不统一的场景。然而问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只有有数据更新的申请,就可能会呈现上述的数据库 + 缓存不统一的状况。

解决方案如下:

更新数据的时候,依据数据的惟一标识,将操作路由之后,发送到一个 jvm 外部队列中。读取数据的时候,如果发现数据不在缓存中,那么将从新读取数据 + 更新缓存的操作,依据惟一标识路由之后,也发送同一个 jvm 外部队列中。

一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,而后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,而后再去更新数据库,然而还没实现更新。此时如果一个读申请过去,读到了空的缓存,那么能够先将缓存更新的申请发送到队列中,此时会在队列中积压,而后同步期待缓存更新实现。

这里有一个优化点,一个队列中,其实多个更新缓存申请串在一起是没意义的,因而能够做过滤,如果发现队列中曾经有一个更新缓存的申请了,那么就不必再放个更新申请操作进去了,间接期待后面的更新操作申请实现即可。

待那个队列对应的工作线程实现了上一个操作的数据库的批改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,而后写入缓存中。

如果申请还在等待时间范畴内,一直轮询发现能够取到值了,那么就间接返回;如果申请期待的工夫超过肯定时长,那么这一次间接从数据库中读取以后的旧值。

高并发的场景下,该解决方案要留神的问题:

1、读申请长时阻塞

因为读申请进行了十分轻度的异步化,所以肯定要留神读超时的问题,每个读申请必须在超时工夫范畴内返回。

该解决方案,最大的危险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在外面,而后读申请会产生大量的超时,最初导致大量的申请间接走数据库。务必通过一些模仿实在的测试,看看更新数据的频率是怎么的。

另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因而须要依据本人的业务状况进行测试,可能须要部署多个服务,每个服务摊派一些数据的更新操作。如果一个内存队列里竟然会挤压 100 个商品的库存批改操作,每隔库存批改操作要消耗 10ms 去实现,那么最初一个商品的读申请,可能期待 10 * 100 = 1000ms = 1s 后,能力失去数据,这个时候就导致读申请的长时阻塞。

肯定要做依据理论业务零碎的运行状况,去进行一些压力测试,和模仿线上环境,去看看最忙碌的时候,内存队列可能会挤压多少更新操作,可能会导致最初一个更新操作对应的读申请,会 hang 多少工夫,如果读申请在 200ms 返回,如果你计算过后,哪怕是最忙碌的时候,积压 10 个更新操作,最多期待 200ms,那还能够的。

如果一个内存队列中可能积压的更新操作特地多,那么你就要加机器,让每个机器上部署的服务实例解决更少的数据,那么每个内存队列中积压的更新操作就会越少。

其实依据之前的我的项目教训,一般来说,数据的写频率是很低的,因而实际上失常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的我的项目,一般来说写申请是非常少的,每秒的 QPS 能到几百就不错了。

理论粗略测算一下

如果一秒有 500 的写操作,如果分成 5 个工夫片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,个别是在 20ms 左右就实现,那么针对每个内存队列的数据的读申请,也就最多 hang 一会儿,200ms 以内必定能返回了。

通过方才简略的测算,咱们晓得,单机撑持的写 QPS 在几百是没问题的,如果写 QPS 扩充了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。

2、读申请并发量过高

这里还必须做好压力测试,确保凑巧碰上上述情况的时候,还有一个危险,就是忽然间大量读申请会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,须要多少机器能力扛住最大的极限状况的峰值。

然而因为并不是所有的数据都在同一时间更新,缓存也不会同一时间生效,所以每次可能也就是多数数据的缓存生效了,而后那些数据对应的读申请过去,并发量应该也不会特地大。

3、多服务实例部署的申请路由

可能这个服务部署了多个实例,那么必须保障说,执行数据更新操作,以及执行缓存更新操作的申请,都通过 Nginx 服务器路由到雷同的服务实例上。

比如说,对同一个商品的读写申请,全副路由到同一台机器上。能够本人去做服务间的依照某个申请参数的 hash 路由,也能够用 Nginx 的 hash 路由性能等等。

4、热点商品的路由问题,导致申请的歪斜

万一某个商品的读写申请特地高,全部打到雷同的机器的雷同的队列外面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,而后才会导致读写并发,所以其实要依据业务零碎去看,如果更新频率不是太高的话,这个问题的影响并不是特地大,然而确实可能某些机器的负载会高一些。

学习材料分享

12 套 微服务、Spring Boot、Spring Cloud 核心技术材料,这是局部材料目录:

  • Spring Security 认证与受权
  • Spring Boot 我的项目实战(中小型互联网公司后盾服务架构与运维架构)
  • Spring Boot 我的项目实战(企业权限治理我的项目))
  • Spring Cloud 微服务架构我的项目实战(分布式事务解决方案)
  • 公众号后盾回复 arch028 获取材料::

退出移动版