天天看点

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

作者:架构师面试宝典
互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

在互联网业务中,传统的直接访问数据库的方式,主要是通过数据分片、一主多从等方式来解决数据库读写并发的问题。但是随着数据量的不断积累,流量不断增加,仅仅依赖于数据库来承受所有的流量不仅成本高而且系统稳定性也会随之降低。所以在一般的架构设计中架构师们都采用增加缓存的操作来提升系统的响应能力,从而提升数据的读写能力、极大程度上减少了对于数据库的读写,减轻了数据库的压力,这样做会带来系统的更好的用户体验。对于缓存来讲,如何能保证与数据库的数据的一致性是在一个应用缓存的时候不得不去解决的问题。

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

需要知道,缓存系统的数据一致性包括了与数据库持久化的一致性和缓存本身之间的数据一致性,以及如果采用了多级缓存,那么多级缓存之间也会存在一致性的问题。而对于持久化与缓存层面的数据一致性问题通常被称为是双写一致性问题。所谓的“双写”也就是在数据库中保存一份数据,同样也在缓存中保存一份数据。

对于数据一致性来讲,包含强一致性和弱一致性,强一致性是指在写入之后立即可以读取到所有的写入数据,而弱一致性是指在数据写入之后,在经历一段时间之后一定能读取到正确的数据。而采用弱一致性的最为广泛的模型就是我们经常提到的数据的最终一致性。也就是说数据写入之后在一段时间内读写的数据都会达到一致性。在大部分的应用场景中大家追求的就是数据的最终一致性,只有在一小部分的应用场景中才会采用强一致性来完成业务需求。那么下面我们就来看看保证最终一致性的策略

旁路缓存(Cache-aside)

Cache-aside 意为旁路缓存模式,是被应用的最广泛的一种缓存策略。如下图所示。

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

从流程上来看,在读请求中,首先需要先去访问缓存,如果在缓存中存在相关的数据则直接返回数据;如果在缓存中没有想过的数据,则需要从数据库中查询到数据,并且将查询到的结果更新到缓存中然后再进行返回。在写入的时候需要先更新数据库的数据,然后将缓存中的数据进行删除。

为什么这里要删除缓存?而不是更新缓存?

在Cache-aside操作中,对于读请求来讲处理起来相对比较简单,但是对于写请求处理,可能会有人问,为什么上面的操作中是删除缓存,而不是更新缓存呢?通常我们需要写入一个数据发生变化的时候只需要更新相关的修改内容即可,这里为什么要删除缓存重新获取呢?

首先来讲,从性能角度上来讲,缓存的结果应该是一个经过了大量计算的耗时结果。例如有些业务中会出现的联合查询问题。而这样的操作如果在写入操作发生之后,更新缓存也将会是一个非常耗时的操作。同时写入的数据如果太多的时候,有可能会出现缓存还没更新完成,就有线程进行数据读取操作,导致数据再次被更新的情况出现,这种现象被称为是换从扰动。很明显这种操作是浪费了太多的机器性能,会导致缓存利用率不是太高。如果是等到缓存未命中的时候去更新缓存,也符合懒加载的设计思路,需要的时候再进行计算。删除缓存操作不仅是幂等的,也可以在发生异常的时候进行重试,而且写-删除和读-更新操作在语义操作是对等的。也就是说执行的结果是一样的。

安全性,在并发场景下,在写请求中更新数据有可能会导致发生数据不一致的问题。如下图所示,如果两个线程同时进行写请求,首先线程1更新了数据库,接下来线程2再次更新数据库的时候,可能会由于网络延迟的问题导致线程1的操作会晚于线程2的操作。也就是说下面步骤中线程1更新缓存的操作,会晚于线程2更新缓存的操作。这样就会导致写入数据库的操作是来自线程2的新值,而写入缓存的结果是来自线程1所带的旧值,也就是说缓存中的数据会落后于数据库中的数据。这个时候如果有请求落在缓存中,就会导致缓存中获取到的旧值而不是新值。

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

为什么一定要先更新数据库,再去删除缓存呢?

对于这个操作,在单线程场景下,来讲如果先删除了缓存再去更新数据库看上去是可行的。因为即使这个时候出现了问题,也只会影响缓存中的数据,因为第二次进入获取缓存如果缓存中没有获取到数据,那么就会再次请求数据库更新缓存操作,并没有对实际结果产生影响。这样一来似乎也没有什么影响。那为什么还要去先更新数据库再删除缓存呢?

上面我们说的是在单线程场景下的情况,如果在并发场景下。就会出现如下的问题。

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

首先来自线程1的写请求删除了缓存,那么接下来进入到线程2的读请求由于在缓存中没有命中数据,就会从数据库中进行查询,并且更新缓存。还是刚刚的问题,由于网络消耗、I/O消耗有可能会出现线程1的写请求会晚于线程2的读请求。这样就会导致最终写入到缓存中的值还是从线程2中获取到的旧值,而在线程1的新值则没有被放入这个时候就会导致缓存中的数据其实是落后于数据库中的数据的,这个时候再去请求数据的时候获取到的就是旧的值,而不是新的值。

另外一点就是如果先删除缓存的话,有可能会导致大量的数据由于未命中缓存,而全部请求到数据库上的情况。会导致缓存击穿或者是雪崩的问题出现。

如果一定要先删除缓存,再更新数据库,如何能够保证数据一致性的问题呢?

在上面的的内容中我们提到了关于先删除缓存,在更新数据库所带来的数据不一致的问题,那么如果一定要采用这种方式,如何来保证数据一致性的问题呢?最简单的想法就是再完成数据缓存更新的时候对缓存进行一次延迟删除操作。什么意思呢?

就是说在数据库更新数据完成之后,延迟一段时间然后再删除数据。这个时候如果数据是正常的,那么更新缓存操作之后就可以拿到正常的数据值,如果数据是不正常的,那么获取到不正常的数据值对于系统本身来讲也是对的,毕竟保证的是最终一致性。最终由于延迟删除缓存操作,再次进行读操作的时候就会将新的值更新到缓存中。需要解决的问题就是如何把控延迟删除缓存的时间。所以一般情况下不推荐使用这种方式。

Cache-Aside会出现数据不一致的情况么?

在Cache-Aside中其实也是存在数据不一致性的可能,只不过这种情况发生的概率比较低。如下图所示,如果线程1的读请求在未命中缓存的情况下查询了数据库,这个时候线程2的写请求更新了数据库,但是由于一些特殊的条件,线程1中的读请求的更新缓存操作晚于线程2中的写请求删除缓存的操作,那么最终导致的结果就是写入缓存中的操作是线程1的旧值,而写入到数据库中的数据是线程2中的新值,

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

出现这种情况的前提是需要缓存失效并且读写并发执行,而且需要读请求的执行早于写请求的执行,同时读请求的执行完成时间晚于写请求的时间。这种情况在实际生产中出现的概率极小。一般可忽略不计。

此外在Cache-Aside,在并发场景下,也存在读请求命中缓存的时间再写请求更新之后删除缓存之前的情况,这样就会导致读请求查询到缓存落后于数据库的情况。虽然在下次读请求的中会更新缓存,如果业务对于这种情况的容忍度较低的情况下,那么可以通过加锁的方式来保证这种情况,但是加锁就会导致吞吐量下降,所以加锁操作会导致损耗一定的性能。

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

图片来源网络

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

图片来源网络

加锁处理逻辑

互联网面试-在高并发场景下如何保证缓存和数据库的最终一致性?

图片来源网络

总结

Cache-Aside 策略是我们日常开发中经常使用到的缓存策略,不过在使用的时候也需要根据实际情况选择符合逻辑的修改。

继续阅读