延迟双删
java项目中,我们往往需要保证Redis缓存和数据库的一致性。
对于写操作,我们有两种解决方案
- 先删除缓存,后修改数据库
- 先修改数据库,后修改缓存
其实这两种方案都会导致脏数据(缓存和数据库数据不一致的现象)的出现,所以我们需要一刀切的方式,直接先删除缓存,后修改数据库,延迟若干时间t,再次删除一次之前的缓存。我们称之为“延迟双删”
我们为什么要延迟时间t呢?原因是实际项目开发中我们的Redis往往是主从模式的,t时间过后,可以保证主节点有时间数据同步到从节点,然后再删除缓存,保证多个Redis节点都和数据库数据的一致性!!
虽然“延迟双删”极大降低了脏数据的风险,但是依旧有脏数据的风险,因为延迟时间t很难有一个恰到好处的值,做不到绝对的强一致性!
对于数据的一致性,有两种解决方案,一种是强一致性的解决方案,一种是最终一致性的解决方法,这取决于业务。一般来说普通业务选择最终一致性的就行了,这种性能最高。强一致性的性能低,需要使用分布式锁或者是读锁+排他锁(read-lock+write-lock),其中读锁是可以高并发读取,排他锁排斥读锁与其他排他锁
强一致性
如果我们想要强一致性,我们可以添加一个分布式锁,但是此时性能会降低很多!
分布式锁方案如下:
直接保证了线程之间按照串行化的流程执行!但是像这种分布式锁会导致读写都添加了锁,因此性能特别低。
下面这种是先删除缓存,后更新数据库的翻车场景
下面这种是先更新数据库,后删除缓存的翻车场景,1的情况是线程1来查询缓存的时候,恰逢Key过期了,没查到,就访问数据库,接着线程2来更新数据库了,然后删除cache,最后线程1写入缓存(步骤4)
那么我们如何针对分布式锁这种读和写都添加锁的性能问题进行优化呢?
我们可以使用共享锁和排他锁,共享锁是读锁,其他线程可以读,排他锁是读写锁,使用排他锁的时候其他线程无法读或者写,都禁止了!
读锁,可读,不可写
写锁,其他线程无法读和写
强一致的解决方案,就是读写锁。就是上面介绍的那些
最终一致性
下面开始介绍最终一致性的解决方案:
1.MQ保证最终一致性
由MQ保证最后更新缓存的时刻。流程是这样的:
修改数据请求到达item-Service,然后写入到MySQL,接着item-Service发布消息到MQ,cache-Service监听消息,如果cache-Service接受到消息了,就会及时去更新缓存。这个过程的可靠性由MQ维持