redis系列-保证缓存与数据库双写一致性,简单介绍如何解决redis的缓存与数据库双写一致性问题。
简介
一般来说,如果允许缓存可以稍微跟数据库偶尔不一致的情况,也就是说你的系统不是严格要求:缓存、数据库必须保持一致性的话,最好不要采取这个方案,读请求和写请求串行化, 串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它会导致系统的吞吐量大幅度降低,要使用比正常大几倍的机器去支撑线上的请求。
项目中缓存使用
最经典的缓存+数据库读写模式,就是Cache Aside Patterm:
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后删掉缓存。
由于缓存中存的结果有的时候不是简单的从数据库中取出存入的,有的复杂的场景下,可能是经过复杂的计算存入的,所以更新数据库时删除缓存而不是更新缓存。
原因及解决方案
原因一
更新数据时,先更新数据库,在删除缓存,如果删除缓存失败了,就会导致数据库中的数据时最新的,而缓存中的数据还是旧的数据。
解决方案
先删除缓存,在更新数据库,如果数据库更新失败了,那么数据库中是旧的数据,缓存中是空的,那么数据不会不一致,读的时候缓存没有,就会去读数据库中的旧数据,然后更新到缓存中。
原因二
更新数据时,先删除了缓存,然后去修改数据库,此时还没修改完,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放在了缓存中,随后修改数据库的进程完成了,导致修改后的数据库数据与缓存中的数据不一致了。只有在高并发的场景进行读写的时候才出现这种问题,
解决方案
更新数据的时候,根据数据的唯一标识,将操作路由后,发送到一个JVM内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新执行“读取数据库数据+更新缓存”的操作,根据唯一标识路由之后,也发送到JVM的内部队列。
一个队列对应一个工作线程,每个工作线程串行拿到相应的操作,然后一条条执行,这样的话,一个数据变更操作,先删除缓存,然后去更新数据库,如果没完成更新,此时有一个读的请求过来,没有读到缓存,可以将请求也放入JVM队列中,然后同步等待缓存更新完成,在执行读取操作。
一个队列中,多个更新缓存请求串在一起没有意义,可以做过滤,如果发现队列里面有一个更新缓存的请求了,就不要在放请求操作进去了,直接等待前面的更新操作完成即可。
如果请求还在等待,不断轮询发现可以取到值,那么直接返回就好了,如果等待请求时间过长,纳闷这一次直接从数据库中读取当前的旧值。
该方案在高并发下注意的问题:
读请求长时间阻塞:如果数据更新频繁,导致队列中积压大量更新操作,读请求会发生大量的超时,导致大量的请求直接走数据库。如果出现这种情况,根据并发量,选择增加服务器来平摊处理。
读请求并发量较高:做好压力测试,如果突然出现大量读请求在几十毫秒的延时内同时请求,做好压力准备。
多服务实例部署的请求路由:可能服务部署了多个实例,必须保证执行数据库更新操作,以及执行缓存更新操作的请求,都通过Nginx服务器路由到相同的服务器实例上。
热点key路由问题,会导致请求倾斜:如果某个商品的读写要求特别高,全部打到相同的机器的相同队列里面去了,可能会造成某台服务器压力特别大。所以要根据实际的业务,如果更新频率不是太高的话,可以选择此方案。