一、技术背景
在实际的项目开发中,我们经常会使用到缓存中间件(如redis、MemCache等)来帮助我们提高系统的可用性和健壮性。
但是很多时候如果项目比较简单,就没有必要为了使用缓存而专门引入Redis等等中间件来加重系统的复杂性。那么使用Java本身自己的轻量级的缓存组件就是完美解决方式。
二、技术效果
- 实现缓存的常见功能
- 热点数据预热
- 简单限流
- 去重
三、ExpiringMap
3.1 ExpiringMap简介
ExpiringMap具有高性能、低开销、零依赖、线程安全、使用 ConcurrentMap 的实现过期 entries 等优点。
功能包括不限于:
- 设置 Map 中的 Entry 在一段时间后自动过期。
- 设置 Map 最大容纳值,当到达 Max size 后,再次插入值会导致 Map 中的第一个值过期。
- 设置 添加监听事件,在监听到 Entry 过期时调度监听函数。
- 设置懒加载,在调用 get() 方法时创建对象。
- 允许您了解条目预计何时过期
3.2 ExpiringMap使用
3.2.1 pom.xml 中添加依赖
<!-- https://mvnrepository.com/artifact/net.jodah/expiringmap --><dependency><groupId>net.jodah</groupId><artifactId>expiringmap</artifactId><version>0.5.10</version></dependency>
3.2.2 代码中使用
/*** ① maxSize:Map存储的最大值,类似队列,容量固定,当操作map容量超出限制时,最开始的元素就会依次过期,只保留最新的;* ② expiration:过期时间;* ③ expirationListener:过期监听,当条目过期时,将同步调用过期侦听器,并且在侦听器完成之前,* 将阻止对映射的写入操作。还可以在单独的线程池中配置和调用异步过期侦听器,而不会阻塞映射操作;* ④ expirationPolicy:过期策略,包括 ExpirationPolicy.ACCESSED 和 ExpirationPolicy.CREATED 两种;* 1)ExpirationPolicy.ACCESSED :每进行一次访问,过期时间就会自动清零,重新计算;* 2)ExpirationPolicy.CREATED:在过期时间内重新 put 值的话,过期时间会清理,重新计算;* ⑤ variableExpiration:可变过期,条目可以具有单独可变的到期时间和策略:*/public static ExpiringMap<String, String> map = ExpiringMap.builder().maxSize(1000).expiration(2, TimeUnit.HOURS).variableExpiration().expirationPolicy(ExpirationPolicy.ACCESSED).expirationListener((key, value) -> {System.out.println("SseEmitter已过期,key:"+ key);}).build();
3.2.3 参数说明
① maxSize:Map存储的最大值,类似队列,容量固定,当操作map容量超出限制时,最开始的元素就会依次过期,只保留最新的;② expiration:过期时间;③ expirationListener:过期监听,当条目过期时,将同步调用过期侦听器,并且在侦听器完成之前,将阻止对映射的写入操作。还可以在单独的线程池中配置和调用异步过期侦听器,而不会阻塞映射操作;④ expirationPolicy:过期策略,包括 ExpirationPolicy.ACCESSED 和 ExpirationPolicy.CREATED 两种;1)ExpirationPolicy.ACCESSED :每进行一次访问,过期时间就会自动清零,重新计算;2)ExpirationPolicy.CREATED:在过期时间内重新 put 值的话,过期时间会清理,重新计算;⑤ variableExpiration:可变过期,条目可以具有单独可变的到期时间和策略;
3.2.4 其他使用方式
//为单个条目指定到期策略:map.put("1", "张三", ExpirationPolicy.CREATED);map.put("2", "李四", ExpirationPolicy.ACCESSED);//variableExpiration 可变过期 条目可以具有单独可变的到期时间和策略:map.put("3", "王五", ExpirationPolicy.ACCESSED, 5, TimeUnit.MINUTES);//过期时间和策略也可以即时更改:map.setExpiration("1", 5, TimeUnit.MINUTES);map.setExpirationPolicy("1", ExpirationPolicy.ACCESSED);//动态添加和删除过期侦听器:ExpirationListener<String, String> connectionCloser = (key, value) -> System.out.println(key+":"+value);//添加侦听器map.addExpirationListener(connectionCloser);//移除侦听器map.removeExpirationListener(connectionCloser);//设置懒加载
// Map<String, String> stringMap = ExpiringMap.builder()
// .expiration(10, TimeUnit.MINUTES)
// .entryLoader(address -> address)
// .build();
// // 通过 EntryLoader 将值加载到map中
// String value = stringMap.get("1");
// System.out.println("value值:"+value);//获取条目的到期时间:单位:毫秒long expiration = map.getExpectedExpiration("1");System.out.println("距离过期时间还有:"+expiration+"毫秒");//重置条目的内部到期计时器:map.resetExpiration("1");//查看设置的过期时间map.getExpiration("1");System.out.println("设置的过期时间:"+map.getExpiration("1"));
测试结果
距离过期时间还有:299999毫秒
设置的过期时间:300000
四、Guava的LoadingCache
4.1 LoadingCache简介
做java的我们都知道Guava是一个编程工具类库,其中包含了很多高质量高性能的工具类和方法。其中,LoadingCache便是一个特别好用的功能,其背后的架构其实就是Guava cache,Guava Cache 是一个全内存的本地缓存实现,它提供了线程安全的实现机制,它可以加载缓存中不存在的数据,本质其实是一个键值对(key-value)的缓存,可以通过key获取到对应的缓存值value。
特点:提供缓存回收机制,监控缓存加载/命中情况,灵活强大的功能,简单易上手的api。
4.2 LoadingCache使用
4.2.1 pom.xml 中添加依赖
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>24.1-jre</version>
</dependency>
4.2.2 代码中使用
public static LoadingCache<Long, User> userCache= CacheBuilder.newBuilder()// 缓存池大小,在缓存数量到达该大小时, 开始回收旧的数据.maximumSize(1000)// 设置时间60s对象没有被读/写访问则对象从内存中删除.expireAfterAccess(60, TimeUnit.SECONDS)// 设置缓存在写入之后 设定时间60s后失效.expireAfterWrite(60, TimeUnit.SECONDS)// 定时刷新,设置时间10s后,当有访问时会重新执行load方法重新加载.refreshAfterWrite(10, TimeUnit.SECONDS)// 移除监听器,缓存项被移除时会触发.removalListener(new RemovalListener() {@Overridepublic void onRemoval(RemovalNotification rn) {// 处理缓存键不存在缓存值时的处理逻辑log.error(rn.getKey() + "remove");}})// 处理缓存键对应的缓存值不存在时的处理逻辑.build(new CacheLoader<Long, User>() {@Overridepublic User load(Long id) {return getById(id);}});public User getUser(Long id) {User user = userCache.get(id);}public ImmutableMap<Long, User > getAll(List<Long> ids) throws ExecutionException {return cache.getAll(ids);}
4.2.3 参数说明
① maximumSize:缓存的k-v最大数据,当总缓存的数据量达到这个值时,就会淘汰它认为不太用的一份数据,会使用LRU策略进行回收;② expireAfterAccess:缓存项在给定时间内没有被读/写访问,则回收,这个策略主要是为了淘汰长时间不被访问的数据;③ expireAfterWrite:缓存项在给定时间内没有被写访问(创建或覆盖),则回收, 防止旧数据被缓存过久;④ refreshAfterWrite:缓存项在给定时间内没有被写访问(创建或覆盖),则刷新;⑤ recordStats:开启Cache的状态统计(默认是开启的);
4.2.4 GET方法
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {try {if (count != 0) { // read-volatileReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {long now = map.ticker.read();//检查entry是否符合expireAfterAccess淘汰策略V value = getLiveValue(e, now);// value是有效的 则返回if (value != null) {// 记录该值的最近访问时间recordRead(e, now);statsCounter.recordHits(1);// 内部实现了定时刷新,若未开启refreshAfterWrite则直接返回valuereturn scheduleRefresh(e, key, hash, value, now, loader);}ValueReference<K, V> valueReference = e.getValueReference();// 如果有别的线程已经在load value,则等到其他线程完成后再取结果if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// 如果没拿到有效的value,则执行加载逻辑;return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {...} finally {postReadCleanup();}}
4.2.5 Load方法
@GwtCompatible(emulated = true)
public abstract class CacheLoader<K, V> {public abstract V load(K key) throws Exception;}
4.3 移除机制
guava做cache时候数据的移除分为被动移除和主动移除两种。
- 被动移除
- 基于大小的移除:数量达到指定大小,会把不常用的键值移除
- 基于时间的移除:expireAfterAccess(long, TimeUnit) 根据某个键值对最后一次访问之后多少时间后移除。expireAfterWrite(long, TimeUnit) 根据某个键值对被创建或值被替换后多少时间移除
- 基于引用的移除:主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除
- 主动移除
- 单独移除:Cache.invalidate(key)
- 批量移除:Cache.invalidateAll(keys)
- 移除所有:Cache.invalidateAll()
如果配置了移除监听器RemovalListener,则在所有移除的动作时会同步执行该listener下的逻辑。
如需改成异步,使用:RemovalListeners.asynchronous(RemovalListener, Executor)。
4.4 其他
- 在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加
- 配置了expireAfterAccess和expireAfterWrite,但在指定时间后没有被移除。
- 删除策略逻辑:
CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为首先线程相对较重,其次某些环境限制线程的创建。
它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。