面试redis和DB数据一致性问题,也是经常被问到的,只要你建立写了redis,如果面试官想问一些场景问题,都会扯到数据一致性问题,今天我们就解读一下这个问题,按照以下思路解读
- 有哪些缓存模式
- 都有哪些优点和缺点
- 如何解决数据不一致
一,有哪些缓存模式
缓存模式常见的有三种,Cache-Aside Pattern(旁路缓存模式),(Read-Through/Write-Through)读写穿透,Write behind(异步缓存写入)
Cache-Aside Pattern(旁路缓存模式),读请求和写请求流程,如下图,写请求:更新的时候,先更新数据库,然后再删除缓存,读请求:读的时候:先读缓存,缓存命中的话,直接返回数据;缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应
Read-Through(读穿透),基本和旁路缓存模式一致,但是不同是多了一层缓存服务(cache provider)
Write-Through(写穿透),同时更新缓存和数据库
Write behind(异步缓存写入),他的读操作和读穿透一样,但是写操作和穿透有很大一点不同,就是他不直接更新数据库,仅仅更新缓存,等到一定时间再去异步更新数据库,他对于一致性要求很低,但是适合修改操作频繁的操作,由于内存操作,所以性能很高
二,都有哪些优点和缺点
- 旁路缓存模式:实现简单,但是要维护数据库和缓存两个存储数据存储
- 读写穿透模式,实现比较复杂,要多维护一个缓存服务(cache provider)
- 读写异步模式,实现比较复杂,有数据不一致问题,但是性能好
Cache-Aside Pattern(旁路缓存模式)的一些问题,首先我们为什么要删除缓存而不是更新缓存,那肯定是有原因的其实他有三点不好的原因
- 在并发情况下,线程A比线程B先更新数据库,但是由于某些原因,线程A比线程B晚更新缓存,就会导致缓存的数据还是老的数据,有了脏数据,而删除就不会有这种情况
- 对于频繁写的场景,缓存就会频繁更新,浪费性能,
- 对于频繁写的场景,缓存的值经过大量计算得到的,但是没有用几次,就会被更新的话,也是一种性能浪费
但是就有人问了,那为什么不先删除缓存,再更新数据库呢,我们来一个读写并发的操作,看图说话
我们看到,当线程A发起写操作,首先删除了缓存,线程B来时读操作,然后发现缓存没有数据,就会读取数据库,然后更新缓存,此时线程A,又过来更新了数据库,就会导致缓存和数据库不一致了,
此时可能面试官就会问,那先更新数据库,再删除缓存,不会有问题吗,其实也有问题,如下图
线程A读取数据A,发现缓存没有数据,就会读取数据库,此时还没有更新缓存,但是线程B,先更新了数据库,由于缓存没有数据,就不涉及删除缓存,但是此时线程A,把刚才读的旧数据,更新到了缓存,就会导致数据不一致问题.但是这也是概率问题,因为写入缓存的速度远远大于更新数据库的速度.
三.如何解决数据不一致
基本上使用Cache-Aside Pattern模式可以解决大部分场景,但是我们其实还可以优化,达到弱一致性,以及最终一致性的效果
- 延时双删策略
- 删除缓存重试机制
- 读取binlog异步删除缓存
延迟双删策略,先删除缓存,再更新数据库,然后等待1秒,再次删除缓存,这个等待时间要根据业务处理时间适当调整,这样就是为了读请求带来的脏数据,可以再第二次删除掉,清除脏数据
删除缓存重试机制,不管是使用双删策略,还是Cache-Aside Pattern模式,如果第二步删除失败,都可能带来数据不一致问题,
因此我们就可以在删除的时候重复删除,当我们删除失败的时候,我们可以把删除的key放入到消息队列中,然后消费消息队列的key,直到删除操作成功,
取binlog异步删除缓存,使用消息队列虽然可以,但是他会浸入我们的业务逻辑,因此我们可以采用binlog日志进行解耦重复删除,如下图
我们可以使用某种机制采集数据库的binlog日志,放入到消息队列,然后搞一个简单的消费者,消费队列的消息,然后删除缓存,这样就可以保证数据最终异性了,
有人就可能问,如果是主从数据库呢,主备的操作可能有一定的延迟,数据可能还没有到从库,读请求就到达了从库,从而获取到了旧值,因此我们可以使用从库进行binlog采集,从而达到删除缓存的效果,如下图
综上的问题,我们最终得出最完美的方案,如下
- 读出缓存是否有数据
- 如果有,直接返回
- 如果没有,则从数据库获取,再更新缓存,然后返回
- 是更新操作,先更新数据库,再删除缓存
- 为了保证上一步删除成功,采用binlog异步删除
- 如果是主从数据库,则使用从库采集binlog异步删除
- 如果是一主多从,采集所有的从库,可以收到一条消息,删除一次,也可以收到最后一台机器的binlog,再进行删除