lusiqi

redis系列-保证缓存与数据库双写一致性,简单介绍如何解决redis的缓存与数据库双写一致性问题。


简介

一般来说,如果允许缓存可以稍微跟数据库偶尔不一致的情况,也就是说你的系统不是严格要求:缓存、数据库必须保持一致性的话,最好不要采取这个方案,读请求和写请求串行化, 串到一个内存队列里去。

串行化可以保证一定不会出现不一致的情况,但是它会导致系统的吞吐量大幅度降低,要使用比正常大几倍的机器去支撑线上的请求。

项目中缓存使用

最经典的缓存+数据库读写模式,就是Cache Aside Patterm:

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后删掉缓存。

由于缓存中存的结果有的时候不是简单的从数据库中取出存入的,有的复杂的场景下,可能是经过复杂的计算存入的,所以更新数据库时删除缓存而不是更新缓存。

原因及解决方案

原因一

更新数据时,先更新数据库,在删除缓存,如果删除缓存失败了,就会导致数据库中的数据时最新的,而缓存中的数据还是旧的数据。

解决方案

先删除缓存,在更新数据库,如果数据库更新失败了,那么数据库中是旧的数据,缓存中是空的,那么数据不会不一致,读的时候缓存没有,就会去读数据库中的旧数据,然后更新到缓存中。

原因二

更新数据时,先删除了缓存,然后去修改数据库,此时还没修改完,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放在了缓存中,随后修改数据库的进程完成了,导致修改后的数据库数据与缓存中的数据不一致了。只有在高并发的场景进行读写的时候才出现这种问题,

解决方案

更新数据的时候,根据数据的唯一标识,将操作路由后,发送到一个JVM内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据库数据+更新缓存”的操作,根据唯一标识路由之后,也发送到JVM的内部队列。

一个队列对应一个工作线程,每个工作线程串行拿到相应的操作,然后一条条执行,这样的话,一个数据变更操作,先删除缓存,然后去更新数据库,如果没完成更新,此时有一个读的请求过来,没有读到缓存,可以将请求也放入JVM队列中,然后同步等待缓存更新完成,在执行读取操作。

一个队列中,多个更新缓存请求串在一起没有意义,可以做过滤,如果发现队列里面有一个更新缓存的请求了,就不要在放请求操作进去了,直接等待前面的更新操作完成即可。

如果请求还在等待,不断轮询发现可以取到值,那么直接返回就好了,如果等待请求时间过长,纳闷这一次直接从数据库中读取当前的旧值。

该方案在高并发下注意的问题:

  • 读请求长时间阻塞:如果数据更新频繁,导致队列中积压大量更新操作,读请求会发生大量的超时,导致大量的请求直接走数据库。如果出现这种情况,根据并发量,选择增加服务器来平摊处理。
  • 读请求并发量较高:做好压力测试,如果突然出现大量读请求在几十毫秒的延时内同时请求,做好压力准备。
  • 多服务实例部署的请求路由:可能服务部署了多个实例,必须保证执行数据库更新操作,以及执行缓存更新操作的请求,都通过Nginx服务器路由到相同的服务器实例上。
  • 热点key路由问题,会导致请求倾斜:如果某个商品的读写要求特别高,全部打到相同的机器的相同队列里面去了,可能会造成某台服务器压力特别大。所以要根据实际的业务,如果更新频率不是太高的话,可以选择此方案。

 评论