5.基于 Caffeine + Redis + Spring Cache 实现分布式二级缓存方案
一、概要:1、一级缓存Guava Cache ,本地缓存。2、二级缓存Redis ,分布式缓存。提供扩展点,可实现为基于其他分布式缓存的方案3、使用方式:注解方式:1、基于Spring Cache进行扩展 ,利用Spring Cache的注解来提高使用的便捷性,同时方便与Java主流开源框架集成注:对于过期时间需要扩展2、自定义实现一套cache注解API方式:...
源码地址:
源码地址:https://gitee.com/ck-jesse/l2cache
具体实现文章见:https://blog.csdn.net/icansoicrazy/article/details/106635446
基于
Caffeine
实现
一、概要:
1、一级缓存
Guava Cache
,Caffeine ,本地缓存。
2、二级缓存
Redis
,分布式缓存。
提供扩展点,可实现为基于其他分布式缓存的方案
3、使用方式:
注解方式:
1、基于
Spring Cache
进行扩展 ,利用Spring Cache
的注解来提高使用的便捷性,同时方便与Java主流开源框架集成
注:对于过期时间需要扩展2、自定义实现一套cache注解
API方式:
提供简洁的缓存API
二、核心流程:
1、先读本地cache
命中缓存,则返回;
未命中缓存,则读
Redis
;
2、读Redis
缓存
命中缓存,先写
本地cache
,再返回;未命中缓存,则读DB;
3、读DB
命中数据,先写本地cache
,再写Redis
,然后返回;
缓存未命中,DB命中时,分为下面两种情况:
当大量请求访问同一个key时,导致大量请求都打到DB上,这种场景为缓存击穿
。
当大量请求访问不同key时,导致大量请求都打到DB上,这种场景为缓存击穿
。
未命中数据,则返回;
缓存和DB都未命中,这种场景为
缓存穿透
。
注:本地cache基于Guava Cache
实现,可实现只阻塞一个加载线程,其他线程阻塞
三、问题分析:
1、缓存击穿怎么处理?
概念:当大量请求访问同一个key时,导致大量请求都打到DB上,这种场景为缓存击穿。
大量请求访问同一个key的缓存击穿场景
分析:
大量请求会造成某一时刻数据库压力过载。
方案:互斥锁同步加载数据。
在第一个请求上使用互斥锁,其他请求均会阻塞至第一个请求加载数据完成(查询数据、缓存数据、释放锁),然后从缓存中获取数据。
由于会阻塞其他线程,所以系统吞吐量下降。
实现:
基于
Guava Cache
实现,重写CacheLoader#load方法来加载数据。只有一个线程加载数据,其他线程阻塞的目的。简单、稳定、高效。
缓存中有旧数据:只阻塞更新数据的线程,其余线程返回旧数据。
缓存中无旧数据:一个线程去加载数据,其余线程都阻塞了。
大量请求访问不同key的缓存击穿场景
分析:
大量请求会造成某一时刻数据库压力过载。
对于大量请求访问同一个key的场景,
Guava Cache
默认加互斥锁同步加载数据,保证最终只有一个请求打到数据库;但是对于大量请求访问不同key的场景,在此基础上还是会有大量请求打到数据,所以关键点是要限制最终打到数据库的请求,此时可采用线程池异步加载数据,保证只有一定的请求同时打到数据库。
方案:
方案一:线程池异步加载数据。
一方面,解决单key被互斥锁同步阻塞的问题,另一方面,解决多key大量请求打到数据库的问题。
方案二:结合Hystrix或Sentinel进行限流和降级,比如一秒来了5000请求,假设设置一秒只能通过2000个请求,那么剩余的3000请求就会走限流逻辑。然后调用自定义的降级逻辑(比如设置默认值之类的),以此来保护最后的数据库不会被大量请求给打死。
实现:
还是基于
Guava Cache
实现,不过要重写CacheLoader#reload()方法,在此方法中将加载数据逻辑交给线程池异步执行。所有的请求都返回旧的数据,这样就不会有请求被阻塞了。缓存中有旧数据:所有线程返回旧数据,线程池异步加载数据。
缓存中无旧数据:一个线程去加载数据,其余线程都阻塞了。所以需要在系统启动时,就预先将数据加载到缓存。
2、缓存穿透怎么处理?
概念:查询不存在数据的现象我们称为缓存穿透。
简单方法:缓存空值;
复杂方法:大数据场景应用较多。在缓存之前在加一层 BloomFilter
,在查询的时候先去 BloomFilter
去查询 key 是否存在,如果不存在就直接返回,存在再走查缓存 -> 查 DB。
3、缓存一致性的问题?
本地Cache
和 Redis
缓存一致性怎么保证?
假设过期时间expireTime=60s;
首先加载数据,添加数据到本地cache,并设置过期时间expireTime=60s,
再往Redis写入数据,并设置过期时间expireTime-1=59s,也就是redis比本地缓存提前1s过期
假设某个时间点有一个请求过来,从本地cache中没有找到数据,再从redis获取数据,若有,则设置本地缓存和过期时间(过期时间从redis数据中获取),若无,则查DB
集群中怎么保证每个节点中本地缓存的一致性?
方案一:
获取缓存的节点从Redis中获取缓存数据和剩余过期时间,然后往本地cache中添加数据,并设置本地cache的过期时间=redis中缓存的剩余过期时间。
方案二:
通过消息队列(kafka/rocketmq)通知其他节点更新缓存。
以便保证不同节点的本地cache的过期时间的一致性。
更多推荐
所有评论(0)