1. 双写一致性问题
分布式缓存涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题。
2. 双写一致性的要求
- 缓存不能读到脏数据
- 缓存可能会读到过期数据,但要在可容忍时间内实现最终一致
- 这个可容忍时间尽可能的小
要想同时满足上面三条,可以采用读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。但是,串行化之后,就会导致系统的吞吐量会大幅度的降低,要用比正常情况下多几倍的机器去支撑线上请求。
3. 更新策略导致的问题
(1)为什么不是更新缓存,而是删除缓存
- 线程安全角度。同时有请求A和请求B,正常情况下A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑;
- 不是热点数据,只是频繁更新的场景,采用懒加载的思想,只有在使用到的时候才加载至缓存,不然浪费性能;
- 压测一些写接口的时候,为了提高性能(吞吐量、QPS、TPS的概念形容,可以更专业一点),更新操作不会设置为更新缓存(项目内的操作记得没错的话,应该是先删除缓存,再将目前全部内容加载到缓存中),减少缓存频繁删除、加载的操作;
(2)先更新数据库,再删除缓存有什么问题
- 假设一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生:缓存刚好失效,A请求查询数据库,得到旧值;B请求更新数据库,并更新缓存,然后A请求再更新缓存。这样,脏数据就产生了,然而上面的情况是假设在数据库写请求比读请求还要快。实际上,工程中数据库的读操作的速度远快于写操作的。要么通过2PC或是Paxos协议保证一致性,要么就是想尽办法降低并发时脏数据的概率,大概是因为2PC太慢,而Paxos又太复杂,综合考虑,Facebook选择了这个第三种方案;
- 假设同时请求A和请求B,请求A更新完数据库,未删除缓存,但是请求B查询读到的是旧的缓存数据怎么办;
- 如果先修改数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致;
如果删除缓存失败,是否可以通过异常捕获等措施进行补偿,保持最终的数据一致性?
(3)先删除缓存,再更新数据库还有什么问题
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。数据库和缓存中的数据又发生了不一致问题。
只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
3. 定论 + 选择
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应;
- 更新的时候,先更新数据库,然后再删除缓存(不管先删除缓存还是先更新数据库,都存在一定时间的脏数据,虽然facebook选择先删除缓存,但是删除缓存然后还没更新数据库,如果立马有请求读取数据库,又重新加载旧数据到缓存,如果设置缓存时间过长或者永久,问题就很大,只能被动删除缓存,所以应该有其他辅助操作;如果是先更新数据库,再删除缓存,那么脏数据存在的时间应该是可接受的);
- 为了避免删除缓存失败,可以使用一些重试机制,如MQ。为了与项目解耦,可以是使用阿里cancle订阅数据库binlog操作;
(a)更新数据库数据;
(b)数据库会将操作信息写入binlog日志当中;
(c)订阅程序提取出所需要的数据以及key;
(d)另起一段非业务代码,获得该信息;
(e)尝试删除缓存操作,发现删除失败;
(f)将这些信息发送至消息队列;
(g)重新从消息队列中获得该数据,重试操作;
参考资料: