缓存常见问题这一篇就够了
一、缓存架构
缓存架构上图是之前项目的缓存架构,加了二个级别的缓存:进程内缓存、分布式缓存。
读操作的思路:判断该缓存是否开启了进程内缓存的开关,开了则先读进程内缓存,没有则读分布式缓存,若还是没有就读DB;
二、进程内缓存
2.1、进程内缓存的优缺点
优点:与分布式缓存相比较,因为缓存数据存储在站点内,所以减少了一次网络开销。
缺点:分布式缓存虽然多了一次网络开销,但是数据仍然是统一存储的,而进程内缓存的数据存了多份,这会导致数据修改时数据不一致性窗口比分布式缓存要长,一致性难保障。
2.2、如何解决进程内缓存的数据一致性问题?
(1)、将更改的通知写进MQ,让所有站点订阅,然后站点自己去DB读取最新的数据
(2)、若是RPC框架,则用RPC框架的广播功能通知所有站点,站点自己再去读取DB
2.3、为什么为什么不能频繁使用进程内缓存?
分层架构设计,有一条准则:站点层、服务层要做到无数据无状态,这样才能任意的加节点水平扩展,数据和状态尽量存储到后端的数据存储服务,例如数据库服务或者缓存服务。
显然进程内缓存违背了这一原则。
2.4、什么时候适合用进程内缓存?
只读数据,例如配置数据,由于这些数据一般不会修改,所以可以考虑使用进程内缓存。
总结:进程内缓存最大的问题是一致性难以保障,比较适合保存只读数据。大部分场景使用进程内缓存的情况下,都可以改用分布式缓存。
三、分布式缓存
3.1、写操作时,缓存应该淘汰,还是修改?
(1)、修改缓存
a、可以减少一次cache miss,但是会加大业务处理的响应
b、最严重的是,在写并发时还可能会出现缓存数据错误
在1和2两个并发写发生时,由于无法保证时序,此时不管先操作缓存还是先操作数据库,都可能出现:
i、请求1先操作数据库,请求2后操作数据库
ii、请求2先set了缓存,请求1后set了缓存
导致,数据库与缓存之间的数据不一致。
(2)、淘汰缓存
会让读数据时多了一次cache miss。
总结:淘汰缓存。一般的情况下读数据时多一次cache miss并不会有问题,仅仅是加大了响应时间,由于硬件水品的提高,一般情况下感受不到。
3.2、写操作时,先操作数据库,还是先操作缓存?
希望保证两个操作的原子性,要么同时成功,要么同时失败。
这演变为一个分布式事务的问题,保证原子性十分困难,很有可能出现一半成功,一半失败,接下来看下,当原子性被破坏的时候,分别会发生什么。
(1)、先淘汰缓存、后操作数据库
第1步失败,第2步成功,会导致,缓存里的还是旧数据,数据库的是新数据,业务无法接受。若第1步成功,第2步失败,会导致缓存清空了,数据库还是旧数据,也只是会多一次cache miss而已,试想一下这里是更新缓存会有什么后果,所以这也是为什么建议淘汰缓存而不是修改缓存的原因之一。
(2)、先操作数据库、后淘汰缓存
第1步成功,第2步失败,会导致,数据库里是新数据,而缓存里是旧数据,业务无法接受。若第1步失败就可以回滚,不会出现数据不一致。
因为redis操作和mysql操作不在一个事务里面,所以保证原子性较麻烦,有人可能想到用分布式事务,但是大可不必。可以采用一方方法,即当redis操作失败时,手动抛出运行时异常。
要注意的是先淘汰缓存,后操作数据库,还可能会导致如下坑:
当读写并发时会出问题。淘汰缓存后,数据库更新事务提交前,若有读操作,则会从数据库中读取旧数据存到缓存,之后数据事务提交,可是旧数据会一直存放在缓存中。
总结:先操作数据库,后淘汰缓存
3.3、DB主从架构下导致的缓存与DB数据不一致性
(1)、为什么出现不一致性
主从同步有延时,在延期的期间,发生cache miss后读取了从库,就会导致数据不一致性了。本质就是数据库主从同步延时。
(2)、该情况的数据不一致性不良影响
没有缓存架构下时,数据库主从延时,只会导致同步期间短暂的数据不一致性,当同步完成后,不一致性不存在。而有了缓存后,缓存写进了不一致性的从库数据,当数据库同步完成后数据不一致性一直存在,直到缓存过期。可见加入缓存后,这个问题不出力妥当,影响会很糟糕。能不能使得加入缓存后,数据不一致性的时间与每加入缓存相当呢?
(3)、解决办法
a、主从同步;
b、通过工具订阅从库的binlog,这里能够最准确的知道,从库数据同步完成的时间。订阅工具是DTS,可以是cannal,也可以自己订阅和分析binlog;
c、从库执行完写操作,向缓存再次发起删除,淘汰这段时间内可能写入缓存的旧数据;
总结:该办法也只能优化,并不能消除数据不一致性。
3.4、主从数据库不一致解决方法
主从数据库不一致性的根本原因就是主从数据库同步延迟。
解决方法如下
(1)、忽略
业务允许范围下,可以忽略这种段时间内的数据不一致性。
(2)、强制读主
搭建高可用主库,读写都在主库,采用缓存来提高读性能并且达到减轻数据库压力的目的。
(3)、选择性读主
强制读主显得过于粗暴,因为只有少数的写才会导致这种数据不一致性。
可以这样做:
a、写主库;
b、将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”;要注意的是, 假设主从延时为1s,这个key的cache超时时间也为1s。
c、读操作时,把哪个库,哪个表,哪个主键这三个信息拼装一个key,到cache里去查询,如果有的话就读主,没有就读从。
四、缓存使用总结
只要有数据冗余的地方,就会有数据不一致性问题。缓存的难点,从我接触的来看,有两点:缓存不一致性、缓存数据不存在。而缓存数据不存在导致的问题可以分为3类:缓存穿透、缓存击穿、缓存雪崩。从使用的感受来看,缓存的坑还挺多的,使用时要根据业务来选择方案,只有符合业务的才是最好的。