Redis 缓存数据一致性的理解和方案

Redis 282 浏览

数据库和缓存读写顺序?

在了解缓存数据一致之前,我们需要先了解数据库和缓存的读写顺序。

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

命中:程序先从缓存中读取数据,如果命中,则直接返回

失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中

更新:程序先更新数据库,然后再删除缓存

关于更新操作,其实问题比较多。通常疑惑的就几种:

1、我们不考虑更新缓存的原因:首先,并发情况下也可能出现数据不是最新的情况。其次,不确定要更新的这个缓存项是否会被经常读取,假设每次更新数据库都会导致缓存的更新,有可能数据还没有被读取过就已经再次更新了,这就造成了缓存空间的浪费。另外,缓存中的值可能是经过一系列计算的,而并不是直接跟数据库中的数据对应的,频繁更新缓存会导致大量无效的计算,造成机器性能的浪费。当然,这种场景也可能用到,结合实际情况选择。

2、先删除缓存,然后再更新数据库

这个方案的问题很明显,假设现在并发两个请求,一个是写请求A,一个是读请求B,那么可能出现如下的执行序列:

请求A删除缓存

请求B读取缓存,发现不存在,从数据库中读取到旧值

请求A将新值写入数据库

请求B将旧值写入缓存

这样就会导致缓存中存的还是旧值,在缓存过期之前都无法读到新值。这个问题在数据库读写分离的情况下会更明显,因为主从同步需要时间,请求B获取到的数据很可能还是旧值,那么写入缓存中的也会是旧值。这样会产生大量脏数据。

3、先更新数据库,然后再删除缓存

到我们最常用的方案了,但是也会导致一致性问题,不过产生脏数据比较少。

我们设有两个请求,请求A是读请求,请求B是写请求,那么可能会出现下述情形:

请求B更新数据库

请求A查数据库,得到旧值

请求A将旧值写入缓存

请求B删除缓存

期间只有请求B更新数据库,还没来得及删除缓存这段时间内会有脏数据,导致数据不一致。但是后面更新操作完成后,立马将缓存删除了,在后面的读请求获取到的就是新的数据了。

我们依然假设有两个请求,请求A是读请求,请求B是写请求,那么可能会出现下述情形:

先前缓存刚好失效

请求A查数据库,得到旧值

请求B更新数据库

请求B删除缓存

请求A将旧值写入缓存

该情况出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。考虑到数据库上的写操作一般都会比读操作要慢得多,写操作还会上锁,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

后话:当然,这都是我们在不考虑热点数据缓存击穿的情况下来讲的。结合业务,也不太可能在热点数据并发量最高的时候去更新数据库删除缓存吧。在并发量不高的时候更新热点数据,缓存删除了后,自己再请求下,初始化一下缓存没有任何问题。

什么是缓数据一致性?

我们通常存储数据是数据库+缓存使用。我们需要保证缓存数据和数据库数据相同,这就是数据一致性。我们将数据分为最终一致和强一致两类,但是真正意义上来讲数据库的数据和缓存的数据是不可能一致的,因为这是两个系统,必然是两步操作。不过我们可以通过某些方法达到相同目的!

如何保证数据强一致?

1、通过2PC或Paxos等一致性协议来达到强一致目的,但是难度很大,很复杂!

2、通过分布式锁来达到目的,但是实现起来同样难度很大,很复杂!

思路其实就是一种串行化的思路,写请求一定要在读请求之前完成,才能保证最新的数据对所有读请求来说是可见的。

比如,"先更新数据库,再删除缓存",从字面上来看,这里有两步操作,因此在数据库更新之前,到缓存被删除这段时间之内,读请求读取到的都是脏数据。

如果要实现这两者的强一致性,只能是在更新完数据库之前,所有的读请求都必须要被阻塞直到缓存最终被删除为止。如果是读写分离的场景,则要在更新完主库之前就开始阻塞读请求,直到主从同步完毕,且缓存被删除之后才能释放。

如何实现这种阻塞?

对于写请求来说,在更新数据库之前,必须要先申请写锁,而其他线程或机器在读取数据之前,必须要先申请读锁。读锁是共享的,写锁是排他的,即如果读锁存在,可以继续申请读锁但无法申请写锁,如果写锁存在,则无论是读锁还是写锁都无法申请。只有实现了这种分布式读写锁,才能保证写请求在完成数据库和缓存的操作之前,读请求不会读取到脏数据。

串行化可以保证不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求,甚至可能会超出你引入缓存所得到的性能提升。

总结:我们不需要追求数据强一致,保证数据最终一致即可。


如何保证数据最终一致?

即使只是保证数据最终一致性,我们也有可能碰到问题。不管是先删库再删缓存,还是先删缓存再删库,都可能出现数据不一致的情况,比如:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

| 双删加超时

除了设置缓存过期时间这种兜底方案之外,如果我们希望尽可能保证缓存可以被及时删除,那么我们必须要考虑对删除操作进行重试。

这样较差的情况是在超时时间内存在不一致,当然这种情况极其少见,可能的原因就是服务宕机。此种情况可以满足绝大多数需求。 当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定时间,比如500毫秒,这样毫无疑问又增加了写请求的耗时。

为了不影响主流程的正常运行,你可能会将这个事情交给一个异步线程或者线程池来执行,但是如果机器此时也宕机了,这个删除操作也就丢失了。由此我们引入异步消息队列,在删除缓存失败的情况下,将删除缓存作为一条消息写入消息队列,然后由消费端进行消费和重试。

| 异步淘汰缓存

通过读取binlog日志来保持一致性。

| 异步消息队列

在确保Redis高可用,并且数据库数据延迟不影响的情况下,可以先直接更新Redis缓存,再通过异步消息队列更新数据库。

|  版权声明:本文为博主原创文章,转载请注明出处。