这篇文章会通过在开发中遇到的不同场景来分析缓存的使用方式。
一、最简单的“读‑写”场景
场景需求
先从一个最简单的缓存需求开始,比如数据被查询后,希望能缓存一段时间,提高接口响应速度、缓解DB压力等。
解决思路
1.首先查询缓存,
2.如果缓存没有命中,则调用业务代码进行数据加载,并将数据写入缓存、设置超时时间;
3.如果缓存命中,则直接返回缓存数据。
优点
方案简单有效
缺点
缓存和数据库中的数据可能不一致,不能及时更新。
总结
适用于对数据实时性不敏感的场景。比如:用来展示的列表数据
二、查询‑删除场景
场景需求
另一个简单的场景,希望数据被查询后,能缓存一段时间,但数据被更新、删除后,希望查询接口能查出最新数据。
解决思路
1.查询时,仍然按照“先查缓存-没命中则加载数据-回写缓存”的流程;
2.数据变更时,删除缓存。
优点
方案简单,能及时更新缓存数据。
缺点
可能出现缓存与DB数据不一致,更多信息
总结
能适用于大部分常见场景,能容忍较低概率的db与缓存数据一定时间内存在不一致的场景。
三、更新场景
场景需求
数据被查询后,能缓存一段时间,
但数据被更新后,不希望删除缓存增加DB穿透次数,希望能立即更新缓存。
解决思路
查询时,仍然按照“先查缓存‑没命中则加载数据‑回写缓存”的流程;
数据变更时,更新缓存。
优点
方案简单,能及时更新缓存数据。
缺点
可能出现缓存与DB数据不一致
总结
能适用于大部分常见场景,
能容忍较低概率的db与缓存数据一定时间内存在不一致的场景
四、数据加载耗时场景
场景需求
当数据加载比较耗时、并发较高时,会形成缓存雪崩,对DB造成较大的压力。
数据加载比较耗时,瞬间高并发没有命中缓存,全部穿透到DB,较长时间的加载加上DB性能的影响、接口超时重试等,形成缓存雪崩。
如果没有额外的隔离保护措施,还有可能引发服务雪崩。
解决思路
主要解决思路是减少穿透到DB的线程数。
当一个JVM进程内,同时存在多个线程,对于同一个缓存key,没有命中缓存,需要从DB中加载数据时,
选其中一个线程作为Master,其余线程均为Slave,
只有Master会真正的执行数据加载,穿透到DB,并最终回写缓存;Slave则仅等待Master取回来的数据。
Master选择:
当线程没有命中,进入加载流程时,往ConcurrentHashMap loadingMap中,能putIfAbsent成功即成为Master,失败
则为Slave。
loadingMap中记录着,某个缓存key,正在进行数据加载中。
Slave如何等待Master加载数据:
Slave通过wait等待Master的信号,Master完成数据加载后,把数据放入LoadingWrapper中,notifyAll通过所有Slave
去LoadingWrapper中取。
其中还需要包括一些必要的超时检测、异常机制,确保Master异常等,不会引起Slave一直等待,保证业务功能不受影
响。
具体实现流程
优点
可以防止缓存雪崩
缺点
阻塞一部分线程
####
五、本地缓存
场景需求
当接口响应时间要求很高、或存在热点key等,连分布式缓存也达不到要求,需要使用本地缓存时。
解决思路
本地缓存与分布式缓存结合使用,形成二级缓存
先查询本地缓存,本地缓存命中则直接返回;
本地缓存没命中,查询分布式缓存,分布式缓存命中,则回写本地,返回数据。
优点
可以提高接口响应速度。
缺点
分布式节点的本地缓存的同步较为复杂,需要引入额外的手段,目前缓存框架没有支持,用户可以自己实现,如数据更新时MQ广播更新的push方式、定时轮训分布式缓存的pull方式等。
总结
适用于数据实时性要求不那么高的场景,本地缓存的超时时间可以较短,如1分钟等,
1分钟超时后穿透到分布式缓存获取,再回写本地缓存,而分布式缓存的数据更新是可以做到实时同步的。