在更新完数据库以后,必然需要对缓存中的键值对进行修改。而这个过程涉及到了各种各样的不一致性。大致有以下四种双写策略:
(1)先更新缓存,再更新数据库; (2)先更新数据库,再更新缓存; (3)先删除缓存,再更新数据库; (4)先更新数据库,再删除缓存;
(1)先更新缓存,再更新数据库
这种策略会涉及到几个问题:
问题一:如果缓存更新成功,而数据库抽风了,因为各种原因更新失败,此时出现缓存与数据库的数据不一致;
问题二:假设有两个线程A和B,都对数据库进行修改,但执行顺序如下:(1)A对缓存进行更新;(2)B对缓存进行更新;(3)B对数据库进行更新;(4)A对数据库进行更新。此时发现缓存的数据是B线程写的,而数据库中的数据是A更新的。由于网络原因,这类现象在高并发场景下很可能出现,因此也会造成不一致。
(2)先更新数据库,再更新缓存
这种策略也会涉及到(1)中ABBA的问题:
问题一:假设有两个线程A和B,都对数据库进行修改,但执行顺序如下:(1)A对数据库进行更新;(2)B对数据库进行更新;(3)B对缓存进行更新;(4)A对缓存进行更新。此时发现数据库的数据是B线程写的,而缓存中的数据是A更新的。
问题二:如果在写数据库较多的业务场景下,这种方式会频繁的向缓存中写入。如果写入数据库的值,要经过复杂运算才能计算出写入缓存中的值,无疑是浪费性能的。
(3)先删除缓存,再更新数据库
这种策略也会导致ABBA不一致的问题:
假设有一个更新线程A,和一个查询线程B,进行了如下操作:(1)A进行写操作,删除了缓存中的数据;(2)B读缓存,发现没有;(3)B去数据库中找到了旧值,并将旧值写入缓存;(4)A对数据库进行更新。此时缓存与数据库又不一致了。
为了解决上述问题,可以采用延迟双删的策略:
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
可以看到,一个写操作在更新完数据库后,sleep一段时间,再对缓存中的数据(高并发下很可能是脏数据)进行删除。
延迟一段时间,是因为要确保读到脏数据的请求结束,来删除写入缓存的脏数据。这个时间的设置要根据读业务逻辑的时间,在此基础上加几十ms。
如果数据库采用读写分离的架构,那么有可能A已经在主库中写好了,但还没有同步到从库上。而B此时读了从库上的数据,也会造成数据不一致。此时可以在主从同步时间的基础上,加个几百ms。
但是最大的问题就是,第二次删除可能会导致删除失败。关于这个问题的解决策略放在最后。
(4)先更新数据库,再删除缓存
这种策略也可能涉及到ABBA的不一致问题:
假设A是读线程,B是写线程:(1)缓存中的键值对失效;(2)A线程读缓存发现没有,从数据库中读到一个旧值;(3)B将新值写入缓存;(4)B删缓存;(5)A将旧值写入到缓存。
但是上述写操作(3)比读操作(2)慢很多,所以发生上述顺序的概率较低。如果真的发生了,也可以采用延迟双删的策略,间隔一段时间后,再删一次缓存。
但是这种策略也面临着和上面一样的问题,最后一次删除失败咋整?
如何解决最后一次缓存删除失败
思想很简单,设计一个有保障的重试机制即可,以上述第四种策略为例,有下面两种方法:
方案一: (1)更新数据库;(2)尝试删除缓存,失败;(3)将需要删除的key生产到一个消息队列;(4)业务代码自身消费要删除的key;(5)重复尝试删除,直到删除成功。
但上述方案一显然有悖业务的设计哲学,让业务代码变得冗余,不如另外起一个非业务代码来进行处理。
方案二: binlog日志可以记录数据库的操作,通过一个程序来监听binlog的修改状况,以提取出操作的数据以及key。这样就可以利用非业务代码来重复地尝试删除缓存,实现了与业务代码的解耦。
参考:https://www.zhihu.com/search?type=content&q=redis%20mysql双写
|