一、问题描述:
当使用DB + cache的架构时,会出现是先更新cache,还是先更新DB的问题。
1.先更新cache,再更新DB:
如果更新cache成功,但是更新DB失败,会导致cache和DB数据的不一致。业务在查询时,查询到的cache数据是更新后的,当cache过期后,经过一次miss,将查询到DB的脏数据。
2.先更新DB,再更新cache:
如果更新DB成功,但是更新cache失败,同样会导致cache和DB数据的不一致。业务在查询时,查询到的cache数据是更新前的脏数据,当cache过期后,经过一次miss,查询到的数据才是正确的数据。
在多线程环境下,假如执行顺序如下:
1.线程A更新DB;
2.然后线程B更新DB;
3.然后线程B更新cache;
4.线程A更新cache。
这样也导致了cache和DB数据的不一致。
二、先淘汰缓存,再更新DB
针对上述问题,有人提出淘汰缓存的策略,具体方法如下:
1.先淘汰cache,如果成功,则更新DB;如果失败则不更新DB,后续可以通过重试来解决失败的问题,但是增加了一次cache的miss。
淘汰策略解决了更新cache的数据一致性问题,但是增加了一次miss cache,淘汰缓存策略特别时候cache和DB数据格式不一样的场景,如cache中的数据是通过DB中的数据计算获得的(如组织架构的树形结构cache,避免对DB的递归查询,需要根据DB计算cache的数据结构),这样也节省了更新缓存的代价。
2.1 并发导致先淘汰缓存的数据不一致性
1.线程A执行数据更新操作,先淘汰cache;
2.线程B执行查询操作,出现了miss cache;
3.线程B查询DB,并将查询到的数据缓存到cache中;
4.线程A此时更新DB。
这样cache的数据和DB的数据出现了不一致,即此时cache保存的是脏数据。
2.2 解决方案一:数据更新queue
针对高并发时cache和DB不一致的问题,可以通过数据更新queue来实现,当要更新数据时,先将要更新的数据key保存到queue中,表示此时数据正在更新,当淘汰cache,执行DB都成功后,将数据从queue队列中删除key。
在数据更新的过程中,先查看cache中是否有对应的key,如果有则返回数据;如果没有数据,则先去queue中查询是否存在对应的key,如果存在则使用while(true)来循环查询cache,并设置超时时间;如果queue中没有对应的key,则判断为miss cache,则读取DB,并将数据缓存到cache中,避免了高并发场景下的数据不一致性。
以上方案也可以解决DB主从延迟的数据一致性问题,即当key在queue中时,程序读取主库,否则读取从库。
2.3 解决方案二:延时双删除策略
采用延时双删除策略,数据更新时具体过程如下:
1.淘汰缓存;
2.更新DB;
3.sleep(100);
4.再次淘汰缓存,避免因为另外线程读取时miss cache导致的脏数据保存到了cache。
但是这种方案有sleep时间难以确定,而且加入步骤4再次淘汰执行失败,也同样导致cache和DB数据的不一致,不过可以通过缓存失效时间来实现最终一致性。
三、先更新DB,再删除cache,即缓存旁路模式(Cache Aside Pattern )
具体操作如下:
1.数据查询
先从cache中获取数据,没有得到,则从DB中获取,成功后将数据放到cache中。
2.数据更新
先更新DB,如果成功,则再执行淘汰缓存,如果执行淘汰缓存失败,则DB回滚。
a.如果数据更新时,淘汰cache失败而不执行DB回滚,可以通过如下方案解决:
方案一:使用MQ保存失败的key
可以通过消息MQ来实现重试,即将淘汰cache失败的key发送到MQ中,然后消费MQ,执行淘汰cache操作,直到淘汰成功。
方案二:使用数据库的binlog
订阅DB的binlog,获取binlog中的数据的key,然后执行淘汰cache中的key,如果失败,则发送key到MQ,后续同方案一。
b.多线程导致的不一致性
线程A执行查询,线程B执行更新,具体过程如下:
1.缓存过期失效;
2.线程A查询,则会miss cache,线程A查询DB得到旧数据,然后准备执行更新cache;
3.线程B更新数据,执行更新DB;
4.线程B淘汰cache;
5.线程A此时执行将旧数据更新cache;
这样会出现数据不一致的情况,但该不一致性出现的条件是,步骤3写入DB操作比步骤2耗时更短,才会出现步骤4先于步骤5。而由于DB的读操作远快于写操作,所以上述场景出现的概率还是很小的。如果这样的概率仍然不可接受,则可以使用“延时双删除策略”双保险策略。