redis之作为缓存的使用(三)
缓存异常:
解决缓存与数据库数据非一致性问题:
数据的一致性:
缓存中有数据:则缓存中的数据要和数据库中的数据相同
缓存中没有数据:数据库中的值必须是最新的值
不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。缓存的读写模式不同时,缓存数据不一致的发生情况不一样,应对的方法也会有所不同,
根据是否接收写请求,我们可以把缓存分成读写缓存和只读缓存。
对于读写缓存的缓存策略,同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致;异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据库就没有最新的数据了。
对于只读缓存来说,如果有数据新增,会直接写入数据库;而有数据删改时,就需要把只读缓存中的数据标记为无效,后续发来对该数据的请求,会从数据库中得到,然后写会缓存,这样后续再访问数据时,就能够直接从缓存中读取了。
如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。这两个操作如果无法保证原子性,也就是说,要不都完成,要不都没完成,此时,就会出现数据不一致问题了
如果先删除缓存,然后修改数据库,如果删除缓存成功,但是修改数据库失败,则对于下一次请求发生缓存缺失,访问数据库则是旧的问题
如果先修改数据库然后删除缓存,如果修改数据库成功,但是删除缓存失败,则下一次对于该数据请求会从redis中取出,得到的为旧值,也会错误。
解决方法:
重试机制:
可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka 消息队列)。当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
按照不同的删除和更新顺序,会出现不同的数据不一致性情况,然后有不同的针对策略:
先删除缓存,在更新数据库:如果缓存数据删除后,还没来得及跟新数据库,一个并发线程读取该数据,则出现缓存缺失,会从数据库中读取,这样得到还是旧的数据,然后写会缓存,等到更新完数据库后,这样会造成
数据库和缓存中的数据不一致了。解决方案:
在线程 A 更新完数据库值以后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。这样就可以将并发线程写入缓存的旧数据删除,然后下次读取数据时,读取发生缓存缺失,这样就从数据库中得到的就是
最新数据了。相当于其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。
先更新数据库,在删除缓存:先跟新数据库,如果还没有来得及跟新缓存,这样并发线程到来时,读到的也是旧的数据,不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。所以,这种情况对业务的影响较小。
删除缓存值或更新数据库失败而导致数据不一致,可以使用重试机制确保删除或更新操作成功。
在删除缓存值、更新数据库的这两步操作中(没有错误,但是有时延,在多线程的情况下),有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删。
如果数据进行删改时,修改的是缓存值这样的情况会是怎么样:
此时相当于将redis缓存当做读写缓存来使用:
1:如果修改缓存成功,修改数据库失败:这样来说访问缓存一直会是最新的值,这样来说短期内没有影响,但是一旦缓存过期或者满容后被淘汰,读请求就会从数据库中重新加载旧值到缓存中,之后的读请求会从缓存中得到旧值,对业务产生影响。
2:如果修改数据库,后修改缓存:修改数据库成功后,但是跟新缓存失败后,此时数据库中是最新值,但缓存中是旧值,后续的读请求会直接命中缓存,得到的是旧值。
对于上面提到的两种情况:针对这种其中一个操作可能失败的情况,也可以使用重试机制解决,把第二步操作放入到消息队列中,消费者从消息队列取出消息,再更新缓存或数据库,成功后把消息从消息队列删除,否则进行重试,以此达到数据库和缓存的最终一致。
对于并发条件来说:
1、先更新数据库,再更新缓存,写+读并发:线程A先更新数据库,之后线程B读取数据,此时线程B会命中缓存,读取到旧值,之后线程A更新缓存成功,后续的读请求会命中缓存得到最新值。这种场景下,线程A未更新完缓存之前,在这期间的读请求会短暂读到旧值,对业务短暂影响。
2、先更新缓存,再更新数据库,写+读并发:线程A先更新缓存成功,之后线程B读取数据,此时线程B命中缓存,读取到最新值后返回,之后线程A更新数据库成功。这种场景下,虽然线程A还未更新完数据库,数据库会与缓存存在短暂不一致,但在这之前进来的读请求都能直接命中缓存,获取到最新值,所以对业务没影响。
3、先更新数据库,再更新缓存,写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。
4、先更新数据库,再更新缓存,写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。
场景1和2对业务影响较小,场景3和4会造成数据库和缓存不一致,影响较为大。即在读写缓存模式下,写+读并发对业务的影响较小,而写+写并发时,会造成数据库和缓存的不一致。
针对场景3和4的解决方案是,对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新数据库和缓存,没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。
读写缓存模式由于会同时更新数据库和缓存,优点是,缓存中一直会有数据,如果更新操作后会立即再次访问,可以直接命中缓存,能够降低读请求对于数据库的压力(没有了只读缓存的删除缓存导致缓存缺失和再加载的过程)。缺点是,如果更新后的数据,之后很少再被访问到,会导致缓存中保留的不是最热的数据,缓存利用率不高(只读缓存中保留的都是热数据),所以读写缓存比较适合用于读写相当的业务场景。