今天我们就来说说缓存一致性问题。这个问题是一个很常见的问题,无论是在工作中还是在面试中。我们今天的主题是:缓存一致性问题的老规矩,提纲:1.什么是缓存一致性问题?我们知道缓存的工作原理是先从缓存中获取数据。数据从慢速设备读取实际数据并将数据放入缓存中。就像这样:但是这样的架构有个问题,因为数据库和缓存是不同的组件,操作必须有顺序,不能像数据库事务一样满足ACID的特性,所以会有数据在缓存和缓存中。数据库中的不一致。缓存一致性问题的表现:对于同一条数据,缓存中的数据与数据库中的数据不一致,所以上升到业务层面,就会出现各种奇怪的现象,比如读取旧数据每次,或者每次读取都是过期数据的副本等。2.写操作的解决方案:只写DB,不写Cache,依赖下次查询先写DB,(同步/异步)再写writetoCacheFirstwritetoCache,thenwritetoDB对于更新操作:先更新DB,再删除Cache先删除Cache,再更新DB,先删除Cache,再更新DB,再删除Cache2.1,只写DB,不写入Cache,依赖于下一次查询。这是我们常见的设计方案。该方案只写数据库,不写缓存,依赖下次请求从数据库中取数据放入缓存。细心的读者已经发现,这种设计可能会引发新的问题:缓存击穿(回顾一下缓存击穿:DB有数据,Cache没有数据,瞬时流量会击穿DB)。这种可能性是存在的,但是可能性比较小,因为缓存击穿的前提是大量请求通过缓存进入数据库层,但是因为我们讨论这个小标题的前提是新写的,一般不会有会有大量的即时流量进来,即使有,也不在本文缓存一致性问题的讨论范围内。2.2.先写数据库,再写缓存。这也是我们常见的设计方案。先写数据库,再写缓存。上图也反映了这一点。所以在这种场景下,当线程1再次读取数据时,读取到的数据会先去缓存。此时缓存的值为1,所以读到的值为1。这时候线程1就懵了。。。。。我刚才不是更新到2了吗?当面临缓存穿透时,我们的解决方案之一是:如果查询数据库中没有数据,我们同意将空数据格式放入缓存中。再次查询时,先查询缓存,发现是空数据格式,则直接返回空,防止数据库被瞬时流量压垮。在这个方案下,还有第二步。数据保存后,需要主动将数据放入缓存,以便下次查询。所以如果你的系统有缓存穿透保护,有可能你需要在写完数据库后记得写缓存。2.3.先写入缓存,再写入数据库。顾名思义,就是写操作。先写入缓存,成功后再写入数据库。那么,如果写入数据库失败怎么办?如果写入失败,下次读取时会读取脏数据。如果写数据库失败,有两种解决方法:删除缓存异步任务,继续写数据库。两种方案都有问题!让我们一一分析。删除缓存。删除缓存失败怎么办?使用异步任务重试删除?那么在重试的时候有没有考虑过这种短期的不一致呢?还是您接受这种数据不一致?你增加了多少系统复杂性?异步任务继续写入数据库。异步任务写入失败怎么办?重试?不断失败的重试怎么办?重试任务存储+定时任务?好的,那么,瞬时数据不一致是否可以接受?你增加了多少系统复杂性?所以这种先写缓存再写数据库的方案一般不会被正式使用。一旦出现问题,就很难保证数据的最终一致性。接下来我们讨论更新数据的情况。2.4.先更新数据库,再删除缓存。在这种情况下,你可能想说这是你现在使用的技术方案,但我想说这个方案有问题。不要惊慌反驳。请看这张图:首先,这个技术方案确实是我们日常开发中最常见的,但是作为开发者,我们也应该了解它存在哪些问题,可以采取哪些对策。先说说我个人对这个解决方案的一些改进。延迟删除缓存。先删除缓存,再更新数据库。延迟双删除策略。定时任务增量/全量更新缓存数据。监控数据库binlog增量数据更新缓存。解决方案一:延迟删除缓存。这种改进方案的优点是可以有效防止数据不一致,但不能完全杜绝。为什么说不能完全杜绝呢?因为查询数据的线程也可能会延迟一定时间更新缓存。这种改进方案的缺点是时间不能严格控制。这个时间需要开发者根据经验给定。第二个缺点是延迟行为可能会给系统引入一些新的依赖。你可能会想问是否可以使用内置的jdk延迟队列呢?是的,但是如果服务在延迟期间重新启动怎么办?第三个缺点是可能会增加系统的复杂度,增加维护成本,降低可读性。方案二:先删除缓存,再更新数据库。下面我们将分别对这个方案进行详细的阐述,这里就不做介绍了。方案三:延迟双删策略。下面我们将分别对这个方案进行详细的阐述,这里就不做介绍了。方案四:定时任务增量/全量更新缓存数据。这种解题方式是最直接暴力的,它的优点是可以保证数据的最终一致性。它的缺点是:可能需要引入分布式调度任务(如果不引入,会出现多个实例同时更新,浪费资源,或者加分布式锁),如果是增量同步,需要一个方法区分哪些数据是增量数据。此方法可能会造成业务入侵和性能影响。如果是全量同步,数据量太大,耗时。严重时可能会造成任务阻塞,加剧数据不一致的问题。经过分析,优势很明显。通常情况下,异步主动更新缓存数据是一种不可接受的方式。但是也会有一些业务场景,数据变化不频繁,但是访问非常频繁,更新数据的更新时间已经在缓存中同步更新了。使用这种将DB数据异步加载到缓存中的策略作为底线是可行的。方案五:监控数据库binlog增量数据更新缓存。这样开发就不再关注缓存层,专注于业务开发,只关注数据库,不关心缓存。可以看出,这种方案对于研发人员来说是比较轻量级的,不需要关心缓存级别,而且这种方案虽然比较重,但是很容易形成统一的方案。2.5.先删除缓存,再更新数据库。这种方法也比较容易理解。先删除缓存数据,再更新数据库数据。如果删除缓存失败,则直接返回失败;如果DB更新失败,只会影响缓存的删除。就这些,下次查询的时候补种就行了。如果是这样,数据库会不会因为缓存数据被删除而崩溃?这种可能性是存在的,但是可能性比较小。另外,这个方案真的能解决问题吗?如果新线程删除缓存后立即查询缓存,新线程发现缓存不存在(刚刚删除),则新线程查询数据库并将数据放入缓存,旧线程删除数据库成功地。这个时候数据库没有数据,但是缓存有数据。2.6.先删除缓存,再更新数据库,再删除缓存。基于2.5,可以在此基础上做一些改进,即延迟双删。延迟双删流程:删除缓存->删除DB->延迟一段时间再删除缓存。延迟双删可以解决大部分问题,但在极端情况下,还是有可能出现问题,导致数据不一致。这里有个问题,延迟一段时间,延迟多长时间?1秒?3秒?这是一个经验值,一般为1s~2s。具体取值视实际监控情况而定。既然是估计值,肯定有误差,所以在极端情况下肯定会出现数据不一致的情况。这个问题的解决方法之前也说过,监控数据库binlog增量数据更新缓存,或者使用异步消息等。3.总结在实际工作中或者面试中,如果有人问你各种没有场景的纯技术问题,比如有人看了上面的解决方案还是会提问,你的解决方案还是存在数据不一致的问题,如何解决?技术是为业务服务的,所以在所有不同的业务场景下,技术的选择和解决方案的设计都是不同的。我们需要问他,具体的业务场景是什么?我们需要根据具体的业务场景选择最合适的技术方案。我们要明确的是,一个技术方案不可能覆盖所有场景,倒闭的技术都是耍流氓。
