db和缓存的先后问题在日常代码中是很常见的,现在来分析一下每种情景的细节
首先排除定时删除缓存的这种操作,因为这样不会有双写问题。
因为不管有多少线程,
(1)缓存没失效的时候都是走的缓存;
(2)然后缓存在某一时刻失效时,线程A先到,发现缓存失效,从DB里面去读,此时读的都是最新的值。
(3)即使在(2)中,正好遇到别的线程B更新db的值,若A在更新之前读,并写入缓存,那这一波仍然用的老值;若A在更新之后读,则赶上了更新这一波,全部用的新值。
一般有四种策略
更新缓存:
1.先更新数据库,再更新缓存。(2个更新线程)
(1)A先更新db
(2)B更新了db
(3)B更新了缓存
(4)A更新了缓存
显然,这会导致后续的读缓存都是读的脏数据。
2.先更新缓存,再更新数据库。(2个更新线程)
(1)A先更新缓存
(2)B再更新缓存。
(3)B更新了db
(4)A更新db
不管(3)(4)谁先走,缓存都是脏数据,显然是不合适的
删除缓存:
3.先更新数据库,再删除缓存。
(2个更新线程)
(1)A先更新db
(2)B更新了db
(3)A删除了缓存
(4)B删除了缓存
这种模式下是不会有问题的,不管(3)(4)谁先走。
还有一种: 一个写,一个读
(1)A先更新db
(2)B读了缓存
(3)A删除缓存
这种情况,因为更新DB的时间较长,有可能有限的若干线程读了缓存脏数据。之后的线程发现缓存失效后,读db新的值然后写到缓存中。然后一致读缓存新值。所以某种意义上讲不算大问题。
有问题的情况:(读、写)
(1)缓存失效了
(2)B读了db的旧值
(3)A更新了db
(4)A删除了缓存
(5)B用旧值写入了缓存
这种情况是类似1,2的严重不一致问题。之后的线程读的都是旧缓存。
但是这种情况,除非(2)的读操作比(3)的更新操作还慢,才能导致(4)在(5)前面,所以概率比较小。
4.先删除缓存,再更新数据库。
有一种极端情况(可以忽略):
(1)A删除了缓存
(2)B删除了缓存
(3)B更新了db
(4)A更新了db
此时db更新的是旧值,这种情况基本不存在,因为缓存操作时间较短,而且(1)(2)都是删缓存操作,A一般情况下可定是比B先更新db。
有问题的情况:
(1)A删除了缓存
(2)B查询发现缓存失效
(3)然后B查询db旧值
(4)写旧值到缓存中
(5)A更新了db的值
因为更新的时间比查询的时间长,所以A更新db之前,缓存已经被写了旧值,会导致严重的不一致性。
解决方法:
1.延时双删策略,延时几秒后再次删除缓存。
(1)删除缓存 (2)更新数据 (3)sleep(1000)(4)删除缓存
方式3中的问题也可以用这种思路解决,保证缓存是被删掉的。如果觉得延时影响了吞吐量,可以再启一个线程异步删除。
2.如果删除缓存失败了怎么办?
(1) 可以设定次数,循环 删除
(2)用消息中间件,如果删除失败就把key发送到消息队列中,然后自己消费自己的消息,确定缓存被删除。
(3)用mysql的canal中间件去订阅binlog日志,从binlog日志中提取需要的数据以及key,然后进行删除。
(4)可以把(2)和(3)结合起来,先去查binlog,然后还失败的发到队列里面去。