本地缓存框架:ConcurrentHashMap,Caffeine、GuavaCache、EhCache总结
一、ConcurrentHashMap
连接
二、Caffeine
介绍
Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库,支持丰富的缓存过期策略,使用的是:TinyLfu淘汰算法。
caffeine的API操作功能和Guava 是基本保持一致的。并且caffeine为了兼容之前使用Guava 的用户,做了一个Guava的Adapter可供兼容。
缓存和ConcurrentMap有点相似,区别是ConcurrentMap将会持有所有加入到缓存当中的元素,直到它们被从缓存当中手动移除。但是,Caffeine的缓存Cache 通常会被配置成自动驱逐缓存中元素,以限制其内存占用。
Caffeine 关联的缓存有三种:Cache,LoadingCache 和 AsyncLoadingCache。 springboot + CaffeineCacheManager 所使用的是 LoadingCache。
Caffeine 配置说明
参数 | 类型 | 描述 |
initialCapacity | int | 初始的缓存空间大小 |
maximumSize | long | 缓存的最大条数 |
maximumWeight | long | 缓存的最大权重 |
expireAfterWrite或expireAfterAccess | duration | 最后一次写入或访问后经过固定时间过期 |
refreshAfterWrite | duration | 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存 |
weakKeys | boolean | 打开 key 的弱引用 |
weakValues | boolean | 打开 value 的弱引用 |
softValues | boolean | 打开 value 的软引用 |
recordStats | 开发统计功能 |
备注:
- weakValues 和 softValues 不可以同时使用。
- maximumSize 和 maximumWeight 不可以同时使用。
- expireAfterWrite 和 expireAfterAccess 同事存在时,以 expireAfterWrite 为准。
特点:
(1)缓存淘汰策略:
提供了三种缓存淘汰策略,分别是基于大小、权重、时间、引用方式、手动清除。
基于大小的方式:
1、可以使用Caffeine.maximumSize(long)方法来指定缓存的最大容量。
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.build();
2、可以使用权重的策略来进行驱逐,
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumWeight(10_000)
.build();
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.weigher((String key, Person value)-> value.getAge());
3.基于时间的方式:
1、expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期
2、expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期
3、expireAfter(Expiry): 自定义策略,过期时间由Expiry实现独自计算。
Caffeine
.newBuilder()
// 二十分钟之内没有被写入,则回收
.expireAfterWrite(20, TimeUnit.MINUTES)
// 二十分钟之内没有被访问,则回收
.expireAfterAccess(20, TimeUnit.MINUTES)
.expireAfter(new Expiry<String, UserInfo>() {
@Override
public long expireAfterCreate(String s, UserInfo userInfo, long l) {
if(userInfo.getAge() > 60){ //首次存入缓存后,年龄大于 60 的,过期时间为 4 秒
return 4000000000L;
}
return 2000000000L; // 否则为 2 秒
}
@Override
public long expireAfterUpdate(String s, UserInfo userInfo, long l, long l1) {
if(userInfo.getName().equals("one")){ // 更新 one 这个人之后,过期时间为 8 秒
return 8000000000L;
}
return 4000000000L; // 更新其它人后,过期时间为 4 秒
}
@Override
public long expireAfterRead(String s, UserInfo userInfo, long l, long l1) {
return 3000000000L; // 每次被读取后,过期时间为 3 秒
}
}
)
.build();
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class UserInfo {
private Integer id;
private String name;
private String sex;
private Integer age;
}
4.基于引用的方式:
Caffeine.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
Caffeine.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
Caffeine.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收。
Caffeine
.newBuilder()
// 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
.weakKeys()
// 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
.weakValues()
// 使用软引用存储值.按照全局最近最少使用的顺序回收
.softValues()
.build();
注意:Caffeine.weakValues()和Caffeine.softValues()不可以一起使用。
5.手动清除:
Cache<String, Object> cache = Caffeine.newBuilder().maximumSize(5).build();
// 单个清除
cache.invalidate("key");
// 批量清除
cache.invalidateAll(Arrays.asList("key", "key1"));
// 清空
cache.invalidateAll();
(2)加载方式
caffeine主要有3种加载方式:
- 手动加载
- 同步加载
- 异步加载
手动加载Cache:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Caffeine 手动加载
*/
public class CaffeineDemo {
public static void main(String[] args) throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
.build();
// 使用java8 Lambda表达式声明一个方法,get不到缓存中的值调用这个方法运算、缓存、返回
String value = cache.get("key", key -> key + "_" + System.currentTimeMillis());
System.out.println(value);
//让缓存到期
Thread.sleep(2001);
// 存在就取,不存在就返回空
System.out.println(cache.getIfPresent("key"));
// 重新存值
cache.put("key", "value");
String key = cache.get("key", keyOne -> keyOne + "_" + System.currentTimeMillis());
System.out.println(key);
// 获取所有值打印出来
ConcurrentMap<String, String> concurrentMap = cache.asMap();
System.out.println(concurrentMap);
// 删除key
cache.invalidate("key");
// 获取所有值打印出来
System.out.println(cache.asMap());
}
}
同步加载LoadingCache:
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @description Caffeine 同步加载
*/
public class CaffenineLoadingCacheDemo {
public static void main(String[] args) throws InterruptedException {
LoadingCache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
// 可以使用java8函数式接口的方式,这里其实是重写CacheLoader类的load方法
.build(new MyLoadingCache());
// 获取一个不存在的kay,让它去调用CacheLoader的load方法
System.out.println(cache.get("key"));
// 等待2秒让key失效
TimeUnit.SECONDS.sleep(2);
System.out.println(cache.getIfPresent("key"));
// 批量获取key,让他批量去加载
Map<String, String> all = cache.getAll(Arrays.asList("key1", "key2", "key3"));
System.out.println(all);
}
static class MyLoadingCache implements CacheLoader {
@Nullable
@Override
public Object load(@NonNull Object key) throws Exception {
return key + "_" + System.currentTimeMillis();
}
}
}
异步加载AsyncLoadingCache:
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* @description Caffeine 异步加载
*/
public class CaffeineAsyncLoadingCacheDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
AsyncLoadingCache<Object, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
// 可以使用java8函数式接口的方式,这里其实是重写CacheLoader的load方法
.buildAsync(new MyCacheLoader());
// 获取一个不存在的kay,让它异步去调用CacheLoader的load方法。这时候他会返回一个CompletableFuture
CompletableFuture<String> future = cache.get("key");
//验证
future.thenAccept(s -> System.out.println("当前的时间为:" + System.currentTimeMillis() + " -> 异步加载的值为:" + s));
// 睡眠2秒让它的key失效
TimeUnit.SECONDS.sleep(2);
// 注意:当使用getIfPresent时,也是返回的CompletableFuture
// 因为getIfPresent从缓存中找不到是不会去运算key既不会调用(CacheLoader.load)方法
// 所以得到的CompletableFuture可能会为null,如果想从CompletableFuture中取值的话.先判断CompletableFuture是否会为null
CompletableFuture<String> completableFuture = cache.getIfPresent("key");
if (Objects.nonNull(completableFuture)) {
System.out.println(completableFuture.get());
}
}
static class MyCacheLoader implements CacheLoader{
@Nullable
@Override
public Object load(@NonNull Object key) throws Exception {
return key + "_" + System.currentTimeMillis();
}
}
}
(3)更新策略
更新策略:在设定多长时间后会自动刷新缓存。
LoadingCache<String, String> build = Caffeine.newBuilder().
//1s后刷新缓存
refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
// TODO 此处可以查询数据库 更新该key对应的value数据
return "1111";
}
});
(4)缓存状态监控
默认是使用Caffeine自带的,也可以自己进行实现。在StatsCounter
接口中,定义了需要打点的方法:
- recordHits:记录缓存命中
- recordMisses:记录缓存未命中
- recordLoadSuccess:记录加载成功(指的是CacheLoader加载成功)
- recordLoadFailure:记录加载失败
- recordEviction:记录淘汰数据
默认:
public class CaffeineStatsCounterDemo {
public static void main(String[] args) throws InterruptedException {
Cache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
.recordStats()
.build();
cache.put("key", "value");
CacheStats cacheStats = cache.stats();
System.out.println(cacheStats.toString());
}
}
自定义:
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import lombok.Data;
import java.util.concurrent.atomic.LongAdder;
/**
* 自定义的缓存状态收集器
*/
@Data
public class MyStatsCounter implements StatsCounter {
private final LongAdder hitCount = new LongAdder();
private final LongAdder missCount = new LongAdder();
private final LongAdder loadSuccessCount = new LongAdder();
private final LongAdder loadFailureCount = new LongAdder();
private final LongAdder totalLoadTime = new LongAdder();
private final LongAdder evictionCount = new LongAdder();
private final LongAdder evictionWeight = new LongAdder();
public MyStatsCounter() {
}
@Override
public void recordHits(int i) {
hitCount.add(i);
System.out.println("命中次数:" + i);
}
@Override
public void recordMisses(int i) {
missCount.add(i);
System.out.println("未命中次数:" + i);
}
@Override
public void recordLoadSuccess(long l) {
loadSuccessCount.increment();
totalLoadTime.add(l);
System.out.println("加载成功次数:" + l);
}
@Override
public void recordLoadFailure(long l) {
loadFailureCount.increment();
totalLoadTime.add(l);
System.out.println("加载失败次数:" + l);
}
@Override
public void recordEviction() {
evictionCount.increment();
System.out.println("因为缓存权重限制,执行了一次缓存清除工作");
}
@Override
public void recordEviction(int weight, RemovalCause cause) {
evictionCount.increment();
evictionWeight.add(weight);
System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为:" + weight);
}
@Override
public void recordEviction(int weight) {
evictionCount.increment();
evictionWeight.add(weight);
System.out.println("因为缓存权重限制,执行了一次缓存清除工作,清除的数据的权重为:" + weight);
}
@Override
public CacheStats snapshot() {
return CacheStats.of(
negativeToMaxValue(hitCount.sum()),
negativeToMaxValue(missCount.sum()),
negativeToMaxValue(loadSuccessCount.sum()),
negativeToMaxValue(loadFailureCount.sum()),
negativeToMaxValue(totalLoadTime.sum()),
negativeToMaxValue(evictionCount.sum()),
negativeToMaxValue(evictionWeight.sum()));
}
private static long negativeToMaxValue(long value) {
return (value >= 0) ? value : Long.MAX_VALUE;
}
}
MyStatsCounter myStatsCounter = new MyStatsCounter();
Cache<String, String> cache = Caffeine.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
.recordStats(()->myStatsCounter)
.build();
cache.put("one", "one");
cache.put("two", "two");
cache.put("three","three");
cache.getIfPresent("ww");
CacheStats stats = myStatsCounter.snapshot();
Thread.sleep(1000);
System.out.println(stats.toString());
(5)淘汰监听
Caffeine<String, UserInfo> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.removalListener((String key, UserInfo value, RemovalCause cause)->{
System.out.println("被清除人的年龄:" + value.getAge() + "; 清除的原因是:" + cause);
}).weigher((String key, UserInfo value)-> value.getAge());
Cache<String, UserInfo> cache = caffeine.build();
cache.put("one", new UserInfo(12, "1"));
cache.put("two", new UserInfo(18, "2"));
cache.put("one", new UserInfo(14, "3"));
cache.invalidate("one");
cache.put("three", new UserInfo(31, "three"));
Thread.sleep(2000);
在Caffeine中被淘汰的原因有很多种:
- EXPLICIT: 这个原因是,用户造成的,通过调用remove方法从而进行删除。
- REPLACED: 更新的时候,其实相当于把老的value给删了。
- COLLECTED: 用于我们的垃圾收集器,也就是我们上面减少的软引用,弱引用。
- EXPIRED:过期淘汰。
- SIZE: 大小淘汰,当超过最大的时候就会进行淘汰。
原理分析:
Java 进程内存是有限的,不可能无限地往里面放缓存对象。这就需要有合适的淘汰算法淘汰无用的对象,为新进的对象留有空间。常见的缓存淘汰算法有 FIFO、LRU、LFU。
优先淘汰掉最久未访问到的数据。缺点是不能很好地应对偶然的突发流量。比如一个数据在一分钟内的前59秒访问很多次,而在最后1秒没有访问,但是有一批冷门数据在最后一秒进入缓存,那么热点数据就会被冲刷掉。
FIFO(First In First Out):先进先出。它是优先淘汰掉最先缓存的数据、是最简单的淘汰算法。缺点是如果先缓存的数据使用频率比较高的话,那么该数据就不停地进进出出,因此它的缓存命中率比较低。
优先淘汰掉最不经常使用的数据,需要维护一个表示使用频率的字段。主要有两个缺点:一、大多数据访问频率比较高,内存堪忧;二、无法合理更新新上的热点数据,比如某个数据历史较多,新旧数据一起需要操作,那内存堪忧。
Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,其特点:高命中率、低内存占用。是一种为了解决传统LFU算法空间存储比较大的问题LFU算法,它可以在较大访问量的场景下近似的替代LFU的数据统计部分,它的原理有些类似BloomFilter。
BloomFilter原理:在BloomFilter中,使用一个大的bit数组用于存储所有key,每一个key通过多次不同的hash计算来映射数组的不同bit位,如果key存在将对应的bit位设置为1,这样就可以通过少量的存储空间进行大量的数据过滤。
在TinyLFU中,把多个bit位看做一个整体,用于统计一个key的使用频率,TinyFLU中的key也是通过多次不同的hash计算来映射多个不同的bit组。在读取时,取映射的所有值中的最小的值作为key的使用频率。
TinyLFU根据最大数据量设置生成一个long数组,然后将频率值保存在其中的四个long的4个bit位中(4个bit位不会大于15),取频率值时则取四个中的最小一个。
Caffeine认为频率大于15已经很高了,是属于热数据,所以它只需要4个bit位来保存,long有8个字节64位,这样可以保存16个频率。取hash值的后左移两位,然后加上hash四次,这样可以利用到16个中的13个,利用率挺高的,或许有更好的算法能将16个都利用到。
过程说明:
- 假设有四个hash函数,每当元素被访问时,将进行次数加1;
- 此时会按照约定好的四个hash函数进行hash计算找到对应的位置,相应的位置进行+1操作;
- 当获取元素的频率时,同样根据hash计算找到4个索引位置;
- 取得四个位置的频率信息,然后根据Count Min取得最低值作为本次元素的频率值返回,即Min(Count);
使用方式:
一、方式一
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
@EnableCaching
@Configuration
public class CaffeineCacheConfig {
/**
* 在 springboot 中使用 CaffeineCacheManager 管理器管理 Caffeine 类型的缓存,Caffeine 类似 Cache 缓存的工厂,
* 可以生产很多个 Cache 实例,Caffeine 可以设置各种缓存属性,这些 Cache 实例都共享 Caffeine 的缓存属性。
* @return
*/
@Bean(name = "caffeineCacheManager")
public CacheManager oneHourCacheManager(){
Caffeine caffeine = Caffeine.newBuilder()
.initialCapacity(10) //初始大小
.maximumSize(11) //最大大小
//写入/更新之后1小时过期
.expireAfterWrite(1, TimeUnit.HOURS);
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setAllowNullValues(true);
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
}
@RestController
public class CaffeineController {
@Autowired
private CaffeineService caffeineService;
@GetMapping("/cache-Data/{key}")
public String cacheDataL(@PathVariable String key) {
return caffeineService.cacheData(key);
}
@GetMapping("/cache-put-Data/{key}")
public String cachePutDataL(@PathVariable String key) {
return caffeineService.cachePutData(key);
}
@GetMapping("/cache-delete-Data/{key}")
public String cacheDelteDataL(@PathVariable String key) {
return caffeineService.deleteCaffeineServiceTest(key);
}
@GetMapping("/getCaffeineServiceTest")
public String getCaffeineServiceTest(String name,Integer age) {
return caffeineService.getCaffeineServiceTest(name,age);
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @Cacheable 触发缓存入口(这里一般放在创建和获取的方法上)
* @CacheEvict 触发缓存的eviction(用于删除的方法上)
* @CachePut 更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
* @Caching 将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
* @CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)
*/
@Service
@CacheConfig(cacheManager = "caffeineCacheManager")
@Slf4j
public class CaffeineService {
@Cacheable(value = "data", key = "#key")
public String cacheData(String key) {
log.info("cacheData()方法执行");
return getCache(key);
}
@CachePut(value = "data", key = "#key")
public String cachePutData(String key) {
log.info("cachePutData()方法执行");
return "cachePutData--" + key;
}
private String getCache(String key) {
try {
log.info("getCache()方法执行");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return key;
}
/**
* CacheEvict删除key,会调用cache的evict
* */
@CacheEvict(cacheNames = "outLimit",key = "#name")
public String deleteCaffeineServiceTest(String name){
String value = name + " nihao";
log.info("deleteCaffeineServiceTest value = {}",value);
return value;
}
/**
* condition条件判断是否要走缓存,无法使用方法中出现的值(返回结果等),条件为true放入缓存
* unless是方法执行后生效,决定是否放入缓存,返回true的放缓存
* */
@Cacheable(cacheNames = "outLimit",key = "#name",condition = "#value != null ")
public String getCaffeineServiceTest(String name,Integer age){
String value = name + " nihao "+ age;
log.info("getCaffeineServiceTest value = {}",value);
return value;
}
}
二、方式二
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
@Configuration
public class CaffeineCacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(5000, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(200)
// 缓存的最大条数
.maximumSize(2000)
.build();
}
}
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class UserInfo {
private Integer id;
private String name;
private String sex;
private Integer age;
public UserInfo(Integer id, String name) {
this.id = id;
this.name = name;
}
}
import com.caffeine.lean.entity.UserInfo;
import com.caffeine.lean.utils.CaffeineUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用户模块接口
*/
@Service
@Slf4j
public class UserInfoService {
//模拟数据库存储数据
private Map<Integer, UserInfo> userInfoMap = new ConcurrentHashMap<>();
@Autowired
private CaffeineUtils caffeineUtils;
public void addUserInfo(UserInfo userInfo) {
log.info("create");
userInfoMap.put(userInfo.getId(), userInfo);
// 加入缓存
caffeineUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
}
public UserInfo getByName(Integer userId) {
// 先从缓存读取
UserInfo userInfo = caffeineUtils.getObjCacheByKey(String.valueOf(userId), UserInfo.class);
if (userInfo != null) {
return userInfo;
}
// 如果缓存中不存在,则从库中查找
log.info("get");
userInfo = userInfoMap.get(userId);
// 如果用户信息不为空,则加入缓存
if (userInfo != null) {
caffeineUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
}
return userInfo;
}
public UserInfo updateUserInfo(UserInfo userInfo) {
log.info("update");
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取旧的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替换内容
if (StringUtils.isNotBlank(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (StringUtils.isNotBlank(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
oldUserInfo.setAge(userInfo.getAge());
// 将新的对象存储,更新旧对象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 替换缓存中的值
caffeineUtils.putAndUpdateCache(String.valueOf(oldUserInfo.getId()), oldUserInfo);
return oldUserInfo;
}
public void deleteById(Integer id) {
log.info("delete");
userInfoMap.remove(id);
// 从缓存中删除
caffeineUtils.removeCacheByKey(String.valueOf(id));
}
}
**
* Caffeine缓存工具类
* Cache 可以有的操作
* V getIfPresent(K key) :如果缓存中 key 存在,则获取 value,否则返回 null。
* void put( K key, V value):存入一对数据 <key, value>。
* Map<K, V> getAllPresent(Iterable<?> var1) :参数是一个迭代器,表示可以批量查询缓存。
* void putAll( Map<? extends K, ? extends V> var1); 批量存入缓存。
* void invalidate(K var1):删除某个 key 对应的数据。
* void invalidateAll(Iterable<?> var1):批量删除数据。
* void invalidateAll():清空缓存。
* long estimatedSize():返回缓存中数据的个数。
* CacheStats stats():返回缓存当前的状态指标集。
* ConcurrentMap<K, V> asMap():将缓存中所有的数据构成一个 map。
* void cleanUp():会对缓存进行整体的清理,比如有一些数据过期了,但是并不会立马被清除,所以执行一次 cleanUp 方法,会对缓存进行一次检查,清除那些应该清除的数据。
* V get( K var1, Function<? super K, ? extends V> var2):第一个参数是想要获取的 key,第二个参数是函数
**/
@Component
public class CaffeineUtils {
@Autowired
Cache<String, Object> caffeineCache;
/**
* 添加或更新缓存
*
* @param key
* @param value
*/
public void putAndUpdateCache(String key, Object value) {
caffeineCache.put(key, value);
}
/**
* 获取对象缓存
*
* @param key
* @return
*/
public <T> T getObjCacheByKey(String key, Class<T> t) {
caffeineCache.getIfPresent(key);
return (T) caffeineCache.asMap().get(key);
}
/**
* 根据key删除缓存
*
* @param key
*/
public void removeCacheByKey(String key) {
// 从缓存中删除
caffeineCache.asMap().remove(key);
}
}
import com.caffeine.lean.entity.UserInfo;
import com.caffeine.lean.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 用户模块入口
*/
@RestController
@RequestMapping
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@GetMapping("/getUserInfo/{id}")
public Object getUserInfo(@PathVariable Integer id) {
UserInfo userInfo = userInfoService.getByName(id);
if (userInfo == null) {
return "没有该用户";
}
return userInfo;
}
@PostMapping("/createUserInfo")
public Object createUserInfo(@RequestBody UserInfo userInfo) {
userInfoService.addUserInfo(userInfo);
return "SUCCESS";
}
@PutMapping("/updateUserInfo")
public Object updateUserInfo(@RequestBody UserInfo userInfo) {
UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo);
if (newUserInfo == null) {
return "不存在该用户";
}
return newUserInfo;
}
@DeleteMapping("/deleteUserInfo/{id}")
public Object deleteUserInfo(@PathVariable Integer id) {
userInfoService.deleteById(id);
return "SUCCESS";
}
}
三、GuavaCache
Google Guava Cache是本地缓存,提供了基于容量、时间、引用的缓存回收方式,内部实现采用LRU算法,基于引用回收很好的利用了java虚拟机的垃圾回收机制.
设计灵感来源ConcurrentHashMap,使用多个segments方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。
Guava Cache和ConcurrentHashMap很相似,但也不完全一样.最基本的区别是ConcurrentHashMap会一直保存所有添加的元素,直至显示地移除.Guava Cache为了限制内存占用,通常都设定为自动回收元素。
注意:springboot 2.x以上就踢出了guava cache.
使用场景
- 愿意消耗一些内存空间来提升速度
- 预料到某些键会被多次查询
- 缓存中存放的数据总量不会超出内存容量
功能:
自动将节点加载至缓存结构中,当缓存的数据超过最大值时,使用LRU算法替换;它具备根据节点上一次被访问或写入时间计算缓存过期机制,缓存的key被封装在WeakReference引用中,缓存的value被封装在WeakReference或SoftReference引用中;还可以统计缓存使用过程中的命中率、异常率和命中率等统计数据。
Guava Cache实质是一个在本地缓存KV数据的LocalCache。而LocalCache的数据结构和ConcurrentHashMap一样,都是采用分segment来细化管理HashMap中的节点Entry。不同的是LocalCahce中的ReferenceEntry节点更为复杂。
常用方法:
- get : 要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值;
- getUnchecked:CacheLoader 会抛异常,定义的CacheLoader没有声明任何检查型异常,则可以 getUnchecked 查找缓存;反之不能;
- getAll :方法用来执行批量查询;
- put : 向缓存显式插入值,Cache.asMap()也能修改值,但不具原子性;
- getIfPresent :该方法只是简单的把Guava Cache当作Map的替代品,不执行load方法;
- put(K key, V value) if cached, return; otherwise create, cache , and return.
- invalidate(Object key); 删除缓存
- invalidateAll(); 清楚所有的缓存,相当远map的clear操作。
- long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。
- CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。
- ConcurrentMap<K, V> asMap(); 将缓存存储到一个线程安全的map中。
特性:
(1) 创建缓存的方式
1.CacheLoader
//LoadingCache类型的缓存,可以使用get(K)或get(K,Callable)方法,并且如果使用的是get(K,Callable)方法,
// 当K值不存在时,使用的是Callable计算值,不走load方法计算,然后将值放入缓存。
LoadingCache<String, String> cache = CacheBuilder
.newBuilder().maximumSize(100)
.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
return getSchema(key);
}
});
private static String getSchema(String key) {
System.out.println("load...");
return key + "schema";
}
2.callable
//Cache类型的缓存只能使用Callable的方式get(K,Callable)方法
Cache<String, String> cache2 = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(100, TimeUnit.SECONDS) // 根据写入时间过期
.build();
try {
String value = cache2.get("key4", new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("i am callable...");
return "i am callable...";
}
});
System.out.println(value);
} catch (ExecutionException e1) {
e1.printStackTrace();
}
private static String getSchema(String key) {
System.out.println("load...");
return key + "schema";
}
2)缓存淘汰策略:
提供了三种缓存淘汰策略,分别是基于大小、权重、时间、引用方式、手动清除。
基于大小的方式:
1、可以使用CacheBuilder.maximumSize(long)方法来指定缓存的最大容量。
CacheBuilder.newBuilder()
// 缓存的最大条数
.maximumSize(1000)
.build();
2、可以使用权重的策略来进行驱逐,
CacheBuilder.newBuilder()
.maximumWeight(10_000)
.build();
CacheBuilder.newBuilder()
.maximumWeight(30)
.weigher((String key, UserInfo value)-> value.getAge());
3.基于时间的方式:
1、expireAfterAccess(long, TimeUnit):在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期
2、expireAfterWrite(long, TimeUnit): 在最后一次写入缓存后开始计时,在指定的时间后过期。
注意:expireAfterWrite时guava重新加载数据时使用的是load方法,不会调用loadAll。
CacheBuilder
.newBuilder()
// 二十分钟之内没有被写入,则回收
.expireAfterWrite(20, TimeUnit.MINUTES)
// 二十分钟之内没有被访问,则回收
.expireAfterAccess(20, TimeUnit.MINUTES)
.build();
注:Guava Cache不会专门维护一个线程来回收这些过期的缓存项,只有在读/写访问时,才去判断该缓存项是否过期,如果过期,则会回收。而且注意,回收后会同步调用load方法来加载新值到cache中。
4.基于引用的方式:
CacheBuilder.newBuilder().weakKeys(); 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().weakValues(); 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
CacheBuilder.newBuilder().softValues(); 使用软引用存储值.按照全局最近最少使用的顺序回收。
CacheBuilder
.newBuilder()
// 使用弱引用存储键.当key没有其他引用时,缓存项可以被垃圾回收
.weakKeys()
// 使用弱引用存储值.当value没有其他引用时,缓存项可以被垃圾回收
.weakValues()
// 使用软引用存储值.按照全局最近最少使用的顺序回收
.softValues().build();
注意:CacheBuilder.weakValues()和CacheBuilder.softValues()不可以一起使用。
5.手动清除:
Cache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(5).build();
// 单个清除
cache.invalidate("key");
// 批量清除
cache.invalidateAll(Arrays.asList("key", "key1"));
// 清空
cache.invalidateAll();
(3)监听
在guava cache中移除key可以设置相应得监听操作,以便key被移除时做一些额外操作。缓存项被移除时,RemovalListener会获取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、键和值。监听有同步监听和异步监听两种 :
同步监听:
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(4)
.removalListener((notification) -> {
if (notification.wasEvicted()) {
RemovalCause cause = notification.getCause();
System.out.println("remove cacase is:" + cause.toString());
System.out.println("key:" + notification.getKey() + "value:" + notification.getValue());
}
}).build(cacheLoader);
loadingCache.getUnchecked("a");
loadingCache.getUnchecked("b");
loadingCache.getUnchecked("c");
loadingCache.getUnchecked("d");//容量是4 吵了自动清除,监听程序清除是单线程。
loadingCache.getUnchecked("e");
异步监听:
CacheLoader<String, String> cacheLoader = CacheLoader.from(String::toUpperCase);
RemovalListener<String,String> listener =(notification) -> {
if (notification.wasEvicted()) {
RemovalCause cause = notification.getCause();
System.out.println("remove cacase is:" + cause.toString());
System.out.println("key:" + notification.getKey() + "value:" + notification.getValue());
}
};
//同步监听
LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder().maximumSize(4)
//异步监控
.removalListener(RemovalListeners.asynchronous(listener, Executors.newSingleThreadExecutor()))
.build(cacheLoader);
loadingCache.getUnchecked("a");
loadingCache.getUnchecked("b");
loadingCache.getUnchecked("c");
loadingCache.getUnchecked("d");//容量是4 吵了自动清除,监听程序清除是单线程。
loadingCache.getUnchecked("e");
(4)统计
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后Cache.stats()方法返回如下统计信息:
- hitRate():缓存命中率;
- hitMiss(): 缓存失误率;
- loadcount() ; 加载次数;
- averageLoadPenalty():加载新值的平均时间,单位为纳秒;
- evictionCount():缓存项被回收的总数,不包括显式清除。
Cache<String, String> cache = CacheBuilder.newBuilder()
// 基于时间失效,写入之后开始计时失效
.expireAfterWrite(2000, TimeUnit.MILLISECONDS)
// 缓存容量
.maximumSize(5)
.recordStats()
.build();
cache.put("key", "value");
CacheStats cacheStats = cache.stats();
System.out.println(cacheStats.toString());
(5)并发操作
GuavaCache通过设置 concurrencyLevel 使得缓存支持并发的写入和读取
LoadingCache<Long, UserInfo> build = CacheBuilder.newBuilder()
// 最大3个 同时支持CPU核数线程写缓存
.maximumSize(3).concurrencyLevel(Runtime.getRuntime().availableProcessors()).build(new CacheLoader<Long, UserInfo>() {
@Override
public UserInfo load(Long id) throws Exception {
// 读取 db 数据
return null;
}
});
(6)更新策略
更新策略:在设定多长时间后会自动刷新缓存。
LoadingCache<String, String> build = CacheBuilder.newBuilder().
//1s内阻塞会返回旧数据
refreshAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {
@Override
public String load(String key) {
// TODO 此处可以查询数据库 更新该key对应的value数据
return null;
}
});
案例:
使用方式一、
<!--guava 依赖-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
import com.google.common.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class GuavaCacheUtils {
@Autowired
Cache<String, Object> guavaCache;
/**
* 添加或更新缓存
*
* @param key
* @param value
*/
public void putAndUpdateCache(String key, Object value) {
guavaCache.put(key, value);
}
/**
* 获取对象缓存
*
* @param key
* @return
*/
public <T> T getObjCacheByKey(String key, Class<T> t) {
//通过key获取缓存中的value,若不存在直接返回null
guavaCache.getIfPresent(key);
return (T) guavaCache.asMap().get(key);
}
/**
* 根据key删除缓存
*
* @param key
*/
public void removeCacheByKey(String key) {
// 从缓存中删除
guavaCache.asMap().remove(key);
}
}
import com.guava.cache.lean.entity.UserInfo;
import com.guava.cache.lean.utils.GuavaCacheUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 用户模块接口
*/
@Service
@Slf4j
public class UserInfoService {
//模拟数据库存储数据
private Map<Integer, UserInfo> userInfoMap = new ConcurrentHashMap<>();
@Autowired
private GuavaCacheUtils guavaCacheUtils;
public void addUserInfo(UserInfo userInfo) {
log.info("create");
userInfoMap.put(userInfo.getId(), userInfo);
// 加入缓存
guavaCacheUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
}
public UserInfo getByName(Integer userId) {
// 先从缓存读取
UserInfo userInfo = guavaCacheUtils.getObjCacheByKey(String.valueOf(userId), UserInfo.class);
if (userInfo != null) {
return userInfo;
}
// 如果缓存中不存在,则从库中查找
log.info("get");
userInfo = userInfoMap.get(userId);
// 如果用户信息不为空,则加入缓存
if (userInfo != null) {
guavaCacheUtils.putAndUpdateCache(String.valueOf(userInfo.getId()), userInfo);
}
return userInfo;
}
public UserInfo updateUserInfo(UserInfo userInfo) {
log.info("update");
if (!userInfoMap.containsKey(userInfo.getId())) {
return null;
}
// 取旧的值
UserInfo oldUserInfo = userInfoMap.get(userInfo.getId());
// 替换内容
if (!StringUtils.isEmpty(oldUserInfo.getName())) {
oldUserInfo.setName(userInfo.getName());
}
if (!StringUtils.isEmpty(oldUserInfo.getSex())) {
oldUserInfo.setSex(userInfo.getSex());
}
oldUserInfo.setAge(userInfo.getAge());
// 将新的对象存储,更新旧对象信息
userInfoMap.put(oldUserInfo.getId(), oldUserInfo);
// 替换缓存中的值
guavaCacheUtils.putAndUpdateCache(String.valueOf(oldUserInfo.getId()), oldUserInfo);
return oldUserInfo;
}
public void deleteById(Integer id) {
log.info("delete");
userInfoMap.remove(id);
// 从缓存中删除
guavaCacheUtils.removeCacheByKey(String.valueOf(id));
}
}
import com.guava.cache.lean.entity.UserInfo;
import com.guava.cache.lean.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 用户模块入口
*/
@RestController
@RequestMapping
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@GetMapping("/getUserInfo/{id}")
public Object getUserInfo(@PathVariable Integer id) {
UserInfo userInfo = userInfoService.getByName(id);
if (userInfo == null) {
return "没有该用户";
}
return userInfo;
}
@PostMapping("/createUserInfo")
public Object createUserInfo(@RequestBody UserInfo userInfo) {
userInfoService.addUserInfo(userInfo);
return "SUCCESS";
}
@PutMapping("/updateUserInfo")
public Object updateUserInfo(@RequestBody UserInfo userInfo) {
UserInfo newUserInfo = userInfoService.updateUserInfo(userInfo);
if (newUserInfo == null) {
return "不存在该用户";
}
return newUserInfo;
}
@DeleteMapping("/deleteUserInfo/{id}")
public Object deleteUserInfo(@PathVariable Integer id) {
userInfoService.deleteById(id);
return "SUCCESS";
}
}
import com.google.common.cache.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@EnableCaching
@Configuration
public class GuavaCacheConfig {
@Bean
public Cache<String, Object> guavaCache() {
return CacheBuilder.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(6000, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
}
使用方式二、
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--guava 依赖-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
import com.google.common.cache.*;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.guava.GuavaCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@EnableCaching
@Configuration
public class GuavaCacheConfig {
@Bean(name = "guavaCacheManager")
public CacheManager oneHourCacheManager(){
CacheBuilder cacheBuilder = CacheBuilder.newBuilder()
.initialCapacity(10) //初始大小
.maximumSize(11) //最大大小
//写入/更新之后1小时过期
.expireAfterWrite(1, TimeUnit.HOURS);
//默认 使用CacheLoader
GuavaCacheManager cacheManager = new GuavaCacheManager();
cacheManager.setCacheBuilder(cacheBuilder);
return cacheManager;
}
}
import com.guava.cache.lean.entity.UserInfo;
import com.guava.cache.lean.service.GuavaCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GuavaCacheController {
@Autowired
private GuavaCacheService guavaCacheService;
@Autowired
private CacheManager cacheManager;
@GetMapping("/users/{name}")
public UserInfo getUser(@PathVariable String name) {
System.out.println("==================");
UserInfo user = guavaCacheService.getUserByName(name);
System.out.println(cacheManager.toString());
return user;
}
}
import com.guava.cache.lean.entity.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
/**
* @Cacheable 触发缓存入口(这里一般放在创建和获取的方法上)
* @CacheEvict 触发缓存的eviction(用于删除的方法上)
* @CachePut 更新缓存且不影响方法执行(用于修改的方法上,该注解下的方法始终会被执行)
* @Caching 将多个缓存组合在一个方法上(该注解可以允许一个方法同时设置多个注解)
* @CacheConfig 在类级别设置一些缓存相关的共同配置(与其它缓存配合使用)
*/
@Service
@CacheConfig(cacheManager = "guavaCacheManager")
@Slf4j
public class GuavaCacheService {
/**
* 获取用户信息(此处是模拟的数据)
*/
/**
* 缓存 key 是 username 的数据到缓存 users 中,
* 如果没有指定 key,则方法参数作为 key 保存到缓存中
*/
@Cacheable(value = "Userdata", key = "#username")
public UserInfo getUserByName(String username) {
UserInfo user = getUserFromList(username);
return user;
}
/**
* 从模拟的数据集合中筛选 username 的数据
*/
private UserInfo getUserFromList(String username) {
List<UserInfo> userDaoList = UserDataFactory.getUserDaoList();
for (UserInfo user : userDaoList) {
if (Objects.equals(user.getName(), username)) {
return user;
}
}
return null;
}
}
import com.guava.cache.lean.entity.UserInfo;
import java.util.ArrayList;
import java.util.List;
public class UserDataFactory {
private UserDataFactory() {
}
private static List<UserInfo> userDtoList;
static {
// 初始化集合
userDtoList = new ArrayList<>();
UserInfo user = null;
for (int i = 0; i < 5; i++) {
user = new UserInfo();
user.setName("star" + i);
user.setAge(23);
userDtoList.add(user);
}
}
public static List<UserInfo> getUserDaoList() {
return userDtoList;
}
}
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
private Integer id;
private String name;
private String sex;
private Integer age;
}
四、EhCache
Ehcache:纯java进程内存缓存框架,具有快速、精干等特点。
核心组件:
- cache manager:缓存管理器
- cache:缓存管理器内可以放置若干cache,存放数据的实质,所有cache都实现了Ehcache接口
- element:单条缓存数据的组成单位
- system of record(SOR):可以取到真实数据的组件,可以是真正的业务逻辑、外部接口调用、存放真实数据的数据库等等,缓存就是从SOR中读取或者写入到SOR中去的。
特点:
- 快速、简单、支持多种缓存策略
- 支持内存和磁盘缓存数据,因为无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过RMI、可插入API等方式进行分布式缓存(比较弱)
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理实例,以及一个实例的多个缓存区域
- 提供Hibernate的缓存实现
Ehcache的缓存数据过期策略
Ehcache采用的是懒淘汰机制,每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期。
使用介绍:
ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!-- 指定一个文件目录,当EhCache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
<diskStore path="java.io.tmpdir"/>
<!--
cache元素的属性:
name:缓存名称
maxElementsInMemory:内存中最大缓存对象数
maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
diskPersistent:是否缓存虚拟机重启期数据
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
-->
<!-- 设定缓存的默认数据过期策略 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="20"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"/>
<cache name="simpleCache"
maxElementsInMemory="1000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="10"
timeToLiveSeconds="20"/>
</ehcache>
可以看看这个文章,挺详细,并且写了关于其在分布式方面的应用
代码