1、简介
Caffine 是一款高性能的近似LFU(最近最少频率使用)准入策略的本地缓存组件,Caffeine的底层数据存储采用ConcurrentHashMap,使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
适用范围:变更频率低、实时性要求低的数据
应用场景:
常用数据的枚举值(如类目);
依赖第三方系统一些不频繁变更的键值对(先在本地缓存中查找,若存在则返回,若不存在再调用第三方系统并存入本地缓存中);
等
2、为什么要使用本地缓存?
原因:随着业务体量的增长,使用的缓存方案一般会经过:
-
第一阶段:无缓存直接查DB;
-
第二阶段:数据同步+Redis;
-
第三阶段:多级缓存 三个阶段。
第1阶段:直接查DB只能用于小流量场景,随着QPS升高,需要引入缓存来减轻DB压力。
第2阶段:一般会通过消息队列将数据同步到Redis,并在数据发生变更时同步更新Redis,业务查询时直接查Redis,如果Redis无数据再去查DB。缺点:该方案的缺点是如果Redis发生故障,缓存雪崩会直接将流量打到DB,可能会进一步将DB打挂,导致业务事故。
第3阶段:将Redis缓存作为二级缓存,在其上再加一层本地缓存,本地缓存未命中再查Redis,Redis未命中再查DB。使用本地缓存的优点是不受外部系统影响,稳定性好。
3、本地缓存的优缺点
优点:相对于分布式缓存,本地缓存访问速度更快,使用本地缓存能够减少和Redis
类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时
缺点:但不支持大数据量存储、数据更新时不好保证各节点数据一致性、数据随应用进程的重启而丢失
优点:
-
高性能:Caffeine的设计目标就是提供高性能的缓存解决方案。它采用了高效的算法和数据结构,使得缓存的读写操作都非常快速,从而能够显著减少对数据库的访问,降低数据库的压力。
-
灵活性:Caffeine支持多种缓存策略,如基于容量、基于时间、基于权重、手动移除、定时刷新等,并提供了丰富的配置选项,可以根据不同的应用场景和需求进行灵活配置。
-
内存友好:Caffeine在缓存管理方面非常注重内存的使用效率,它采用了近乎最佳的命中率算法,以减少不必要的内存占用,并提供了内存使用情况的统计和监控功能,有助于优化缓存的使用。
-
易于集成:Caffeine可以与多种Java框架和库进行无缝集成,如Spring等,使得在项目中引入和使用Caffeine变得非常简单和方便。
缺点:
-
存储容量有限:由于Caffeine是本地缓存,它的存储容量受限于应用进程的内存空间。因此,对于需要存储大量数据的应用场景,Caffeine可能无法满足需求。
-
可靠性较低:本地缓存的数据随应用进程的重启而丢失,这意味着如果应用进程崩溃或重启,缓存中的数据将会丢失。这可能导致数据的不一致性和可靠性问题。
-
无法共享(不适用于分布式系统共享 ) :本地缓存只支持被该应用进程访问,一般无法被其他应用进程访问。因此,在需要多个进程或服务器之间共享缓存数据的场景下,Caffeine可能不是最佳选择。
4、常见淘汰策略
FIFO(First In First Out)
:先进先出
LRU(Least Recently Used)
:最近最少使用(基于时间),存在的问题是如果一个数据在1分钟之前被大量访问,这1分钟内没有访问,那么这个热点数据就会被淘汰
LFU(Least Frequently Used)
:最近最少频率使用(基于访问频率),与LRU的区别是,LRU基于访问时间,LFU基于访问频率。LFU利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰,解决了LRU不能处理时间段的问题。
这三种算法的命中率一个比一个好,但实现成本也一个比一个高,实际实现中一般选择中间的LRU。
5、基本用法
(1)引入依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
(2)属性
-
缓存初始容量
initialCapacity :整数,表示缓存初始容量
-
最大容量
maximumSize :最大容量,如果缓存中的数据量超过这个数值,Caffeine 会有一个异步线程来专门负责清除缓存
-
最大权重
maximumWeight:最大权重,存入缓存的每个元素都要有一个权重值,当缓存中所有元素的权重值超过最大权重时,就会触发异步清除
weigher:设置权重策略
public class Test {
public static void main(String[] args) throws InterruptedException {
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.weigher((String key, Person value)-> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.put("three", new Person(1, "three"));
Thread.sleep(10);
//返回缓存中数据的个数
System.out.println(cache.estimatedSize());
System.out.println(cache.getIfPresent("one"));
System.out.println(cache.getIfPresent("two"));
System.out.println(cache.getIfPresent("three"));
}
}
输出:
2
null
Person(18, "two")
Person(1, "three")
比如上述代码中,caffeine 设置了最大权重值为 30,然后将每个 Person 对象的 age 年龄作为权重值,所以整个意思就是:缓存中存储的是 Person 对象,但是限制所有对象的 age 总和不能超过 30,否则就触发异步清除缓存。 注意!!!:特别要注意一点:最大容量 和 最大权重 只能二选一作为缓存空间的限制
(3)API
获取:
V getIfPresent(K key)
:如果缓存中 key 存在,则获取 value,否则返回 null
V get(K var1, Function<? super K, ? extends V> var2)
:如果缓存中不存在该 key 则该函数将用于提供默认值,该值在计算后插入缓存中
Map<K, V> getAllPresent(Iterable<?> var1)
:参数是一个迭代器,表示可以批量查询缓存存值:
void put( K key, V value)
:存入一对数据 <key, value>
void putAll( Map<? extends K, ? extends V> var1)
:批量存入缓存清除:
void invalidate(K var1)
:删除某个 key 对应的数据
void invalidate(Iterable<?> var1)
:批量删除数据
void invalidateAll()
:清空缓存其他:
long estimatedSize()
:返回缓存中数据的个数
CacheStats stats()
:返回缓存当前的状态指标集
ConcurrentMap<K, V> asMap()
:将缓存中所有的数据构成一个 map
void cleanUp()
:会对缓存进行整体的清理,比如有一些数据过期了,但是并不会立马被清除,所以执行一次 cleanUp 方法,会对缓存进行一次检查,清除那些应该清除的数据
(4)四种缓存添加策略
手动加载
// 初始化缓存,设置了 1 分钟的写过期,100 的缓存最大个数
Cache<Integer, Integer> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
int key = 1;
// 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
System.out.println("不存在值,返回null:" + cache.getIfPresent(key)); -> null
// 也可以使用 get 方法获取值,该方法将一个参数为 key 的 Function 作为参数传入。
// 如果缓存中不存在该 key 则该函数将用于提供默认值,该值在计算后插入缓存中:
System.out.println("返回默认值:" + cache.get(key, a -> 2)); -> 2
// 校验 key 对应的 value 是否插入缓存中
System.out.println("返回key对应的value:" + cache.getIfPresent(key)); -> 2
// 手动 put 数据填充缓存中
int value = 4;
cache.put(key, value);
// 使用 getIfPresent 方法从缓存中获取值。如果缓存中不存指定的值,则方法将返回 null
System.out.println("返回key对应的value:" + cache.getIfPresent(key)); -> 4
// 移除数据,让数据失效
cache.invalidate(key);
System.out.println("返回key对应的value:" + cache.getIfPresent(key)); -> null
自动加载:build后加入参数
// 自动加载:为不存在的缓存元素添加默认值
LoadingCache<String, Object> cache2 = Caffeine
.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build((res)-> "Hello world");
String key2 = "dragon";
// 查找缓存,如果缓存不存在则生成缓存元素, 如果无法生成则返回 null
Object value = cache2.get(key2);
System.out.println(value);
List<String> keys = Arrarys.asList("dragon1","dragon2");
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<String, Object> objectMap = cache2.getAll(keys);
System.out.println(objectMap);
输出:
Hello world
{dragon1=Hello world, dragon2=Hello world}
异步手动:buildAsync()
@Test
public void test() throws ExecutionException, InterruptedException {
AsyncCache<String, Integer> cache = Caffeine.newBuilder().buildAsync();
// 会返回一个 future对象, 调用 future 对象的 get 方法会一直卡住直到得到返回,和多线程的 submit 一样
CompletableFuture<Integer> ageFuture = cache.get("张三", name -> {
System.out.println("name:" + name);
return 18;
});
TimeUnit.SECONDS.sleep(10);
Integer age = ageFuture.get();
System.out.println("age:" + age);
}
输出:
name:张三
age:18
异步自动:buildAsync后加参数
@Test
void contextLoads() throws Exception {
AsyncLoadingCache<String, Object> cache = Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.buildAsync(key -> key + "-" + System.currentTimeMillis());
// 获取不存在的 key 时,会使用 buildAsync() 方法中的算法计算出 value,存入缓存
// 异步获取的结果存放在 CompletableFuture 对象中,可以使用 thenAccept() 获取结果
CompletableFuture<Object> future = cache.get("key1");
future.thenAccept(o -> System.out.println(System.currentTimeMillis() + "-" + o));
TimeUnit.SECONDS.sleep(3);
// 不存在则返回 null
CompletableFuture<Object> key2 = cache.getIfPresent("key2");
System.out.println(key2);
}
(5)数据过期策略
expireAfterAccess:最后一次访问之后,隔多久没有被再次访问的话,就过期。访问包括了 读 和 写。
expireAfterWrite:某个数据在多久没有被更新后,就过期。
expireAfter:自定义缓存策略,满足多样化的过期时间要求
Cache<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.expireAfter(new Expiry<String, Person>() {
@Override
public long expireAfterCreate(String s, Person person, long l) {
// 首次存入缓存后,年龄大于 60 的,过期时间为 4 秒
if(person.getAge() > 60){
return 4000000000L;
}
return 2000000000L; // 否则为 2 秒
}
@Override
public long expireAfterUpdate(String s, Person person, long l, long l1) {
// 更新 one 这个人之后,过期时间为 8 秒
if(person.getName().equals("one")){
return 8000000000L;
}
return 4000000000L; // 更新其它人后,过期时间为 4 秒
}
@Override
public long expireAfterRead(String s, Person person, long l, long l1) {
// 每次被读取后,过期时间为 3 秒
return 3000000000L;
}
})
.build();
(6)引用回收
AsyncCache 和 AsyncLoadingCache 缓存不支持软引用和弱引用。
weakKeys():将缓存的 key 使用弱引用包装起来,只要 GC 的时候,就能被回收。
weakValues():将缓存的 value 使用弱引用包装起来,只要 GC 的时候,就能被回收。
softValues():将缓存的 value使用软引用包装起来,只要 GC 的时候,有必要,就能被回收。
(7)自动刷新
refreshAfterWrite(long duration, TimeUnit unit)
:写操作完成后多久才将数据刷新进缓存中,两个参数只是用于设置时间长短的。只适用于 LoadingCache 和 AsyncLoadingCache,如果刷新操作没有完成,读取的数据只是旧数据
(8)数据移除时的监听器(removalListener)
当缓存中的数据发送更新,或者被清除时,就会触发监听器
在监听器里可以自定义一些处理手段,比如打印出哪个数据被清除,原因是什么。这个触发和监听的过程是异步的,就是说可能数据都被删除一小会儿了,监听器才监听到
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.removalListener((String key, Person value, RemovalCause cause) -> {
System.out.println("被清除人的年龄:" + value.getAge() + "; 清除的原因是:" + cause);
})
.weigher((String key, Person value) -> value.getAge());
Cache<String, Person> cache = caffeine.build();
清除的原因有 5 个,存储在枚举类 RemovalCause 中:
EXPLICIT : 表示显式地调用删除操作,直接将某个数据删除 REPLACED:表示某个数据被更新 EXPIRED:表示因为生命周期结束(过期时间到了),而被清除 SIZE:表示因为缓存空间大小受限,总权重受限,而被清除 COLLECTED : 用于我们的垃圾收集器,也就是我们上面减少的软引用,弱引用
(9)同步监视器(需自定义)
之前的 removalListener 是异步监听,此处的 writer 方法可以设置同步监听器,同步监听器一个实现了接口 CacheWriter 的实例化对象,我们需要自定义接口的实现类
//关键是要实现 CacheWriter 接口的两个方法,当新增,更新某个数据时,会同步触发 write 方法的执行。当删除某个数据时,会触发 delete 方法的执行
public class MyCacheWriter implements CacheWriter<String, Person> {
@Override
public void write(String s, Person person) {
System.out.println("新增/更新了一个新数据:" + person.getName());
}
@Override
public void delete(String s, Person person, RemovalCause removalCause) {
System.out.println("删除了一个数据:" + person.getName());
}
}
@org.junit.Test
public void referenceTest() throws InterruptedException {
Caffeine<String, Person> caffeine = Caffeine.newBuilder()
.maximumWeight(30)
.writer(new MyCacheWriter())
.weigher((String key, Person value) -> value.getAge());
Cache<String, Person> cache = caffeine.build();
cache.put("one", new Person(12, "one"));
cache.put("two", new Person(18, "two"));
cache.invalidate("two");
}
(10)统计
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.recordStats()
.build();
通过使用Caffeine.recordStats()
, 可以转化成一个统计的集合. 通过 Cache.stats()
返回一个CacheStats
。CacheStats提供以下统计方法:
hitRate(): 返回缓存命中率 evictionCount(): 缓存回收数量 averageLoadPenalty(): 加载新值的平均时间
SpringBoot 集成 Caffeine
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
在启动类上开启缓存支持:添加@EnableCaching注解开启缓存支持
@SpringBootApplication
@EnableCaching
public class SingleDatabaseApplication {
public static void main(String[] args) {
SpringApplication.run(SingleDatabaseApplication.class, args);
}
}
配置文件的方式注入相关参数
spring.cache.cache-names=cache1
#数组
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=500,expireAfterWrite=10s
创建缓存管理类:
注意!!!:一个spring项目中只能有一个CacheManager实例 , @Cacheable @CachePut @CacheEvict 注解就是根据这个CacheManager实例选择缓存的组件,也就是说如果选择spring-boot-starter-cache依赖的@Cacheable @CachePut @CacheEvict注解减少代码的侵入,只能选择本地缓存Caffeine或Redis缓存中的一个,另一个需要手写缓存逻辑。
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(
Caffeine.newBuilder()
.initialCapacity(1000)
.maximumSize(10000)
.removalListener((key,value,cause) -> {
log.info("cacheManager 键:{},值:{},被清除了,清除原因是:{}",key,value,cause);
})
);
return caffeineCacheManager;
}
}
使用
注意!!!:addUserInfo方法
这里需要修改返回值的类型,不能返回void类型,否则会缓存一个空对象到缓存中对应的key
上。当下次执行查询操作时,会直接返回空对象给调用方!!!
@Service
public class CaffineManagerService {
//模拟数据库
private HashMap<String, UserInfo> userInfoMap = new HashMap<>();
//这种写法是错误的,当返回值是 void 时,缓存一个 空对象 到缓存中对应的`key`上
@CachePut(cacheNames ="user" ,key = "T(String).valueOf(#userInfo.id)")
public void addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
}
//!!!应该改为: 返回值为UserInfo
@CachePut(cacheNames ="user" ,key = "T(String).valueOf(#userInfo.id)")
public UserInfo addUserInfo(UserInfo userInfo) {
userInfoMap.put(userInfo.getId(), userInfo);
return userInfo;
}
@Cacheable(cacheNames = "user",key = "#id")
public UserInfo getByName(String id) {
// 如果缓存中不存在,则从库中查找
return userInfoMap.get(id);
}
@CacheEvict(cacheNames = "user",key = "#id")
public void deleteById(String id) {
userInfoMap.remove(id);
}
}
SpringBoot 集成 Caffeine+Redis实现一二级缓存
注意!!!:一个spring项目中只能有一个CacheManager实例 , @Cacheable @CachePut @CacheEvict 注解就是根据这个CacheManager实例选择缓存的组件,也就是说如果选择spring-boot-starter-cache依赖的@Cacheable @CachePut @CacheEvict注解减少代码的侵入,只能选择本地缓存Caffeine或Redis缓存中的一个,另一个需要手写缓存逻辑。
(1)依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>
<!-- 使用cacheManage会用到下面的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
!!!(2)方式一:自定义注解方式:代码无侵入
定义缓存
@Configuration
@Slf4j
public class CacheConfig {
@Value("${spring.redis.host:#{'127.0.0.1'}}")
private String hostName;
@Value("${spring.redis.port:#{6379}}")
private int port;
@Value("${spring.redis.password:#{123456}}")
private String password;
@Value("${spring.redis.timeout:#{3000}}")
private int timeout;
@Value("${spring.redis.lettuce.pool.max-idle:#{16}}")
private int maxIdle;
@Value("${spring.redis.lettuce.pool.min-idle:#{1}}")
private int minIdle;
@Value("${spring.redis.lettuce.pool.max-wait:#{16}}")
private long maxWaitMillis;
@Value("${spring.redis.lettuce.pool.max-active:#{16}}")
private int maxActive;
@Value("${spring.redis.database:#{0}}")
private int databaseId;
@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
RedisConfiguration redisConfiguration = new RedisStandaloneConfiguration(
hostName, port
);
// 设置选用的数据库号码
((RedisStandaloneConfiguration) redisConfiguration).setDatabase(databaseId);
// 设置 redis 数据库密码
((RedisStandaloneConfiguration) redisConfiguration).setPassword(password);
// 连接池配置
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setMaxTotal(maxActive);
poolConfig.setMaxWaitMillis(maxWaitMillis);
LettucePoolingClientConfiguration lettucePoolingClientConfiguration
= LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(Duration.ofMillis(timeout))
.build();
// 根据配置和客户端配置创建连接
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisConfiguration, lettucePoolingClientConfiguration);
return factory;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
//创建模版
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//设置连接工厂
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 使用 FastJsonRedisSerializer 来序列化和反序列化redis 的 value的值
FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setCharset(StandardCharsets.UTF_8);
serializer.setFastJsonConfig(fastJsonConfig);
// key 的 String 序列化采用 StringRedisSerializer
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// value 的值序列化采用 fastJsonRedisSerializer
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean("caffeineCache")
public Cache<Object,Object> caffeine(){
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.SECONDS)
.removalListener((key,value,cause) -> {
log.info("caffeineCache 键:{},值:{},被清除了,清除原因是:{}",key,value,cause);
})
.build();
}
}
模仿spring
通过注解管理缓存的方式,我们也可以选择自定义注解,然后在切面中处理缓存,从而将对业务代码的入侵降到最低。
//作用在方法或类上
@Target({ElementType.METHOD,ElementType.TYPE})
//运行时起作用
@Retention(RetentionPolicy.RUNTIME)
//这个注解表示当一个类被标注了此注解,它的子类将自动继承这个注解
@Inherited
//当使用javadoc生成文档时,该注解会被包含在生成的文档中
@Documented
public @interface DoubleCache {
@AliasFor("cacheName")
String value() default "";
@AliasFor("value")
String cacheName() default "";
String key() default ""; //支持springEl表达式
long l2TimeOut() default 120;
CacheType type() default CacheType.FULL;
}
我们使用cacheName + key
作为缓存的真正key
(仅存在一个Cache
中,不做CacheName
隔离),l2TimeOut
为可以设置的二级缓存Redis
的过期时间,type
是一个枚举类型的变量,表示操作缓存的类型,枚举类型定义如下:
public enum CacheType {
FULL, //存取
PUT, //只存
DELETE //删除
}
因为要使key
支持springEl
表达式,所以需要写一个方法,使用表达式解析器解析参数:
public static String parse(String elString, TreeMap<String,Object> map){
elString=String.format("#{%s}",elString);
//创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
//通过evaluationContext.setVariable可以在上下文中设定变量。
EvaluationContext context = new StandardEvaluationContext();
map.entrySet().forEach(entry->
context.setVariable(entry.getKey(),entry.getValue())
);
//解析表达式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
//使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
String value = expression.getValue(context, String.class);
return value;
}
参数中的elString
对应的就是注解中key
的值,map
是将原方法的参数封装后的结果。简单进行一下测试:
public void test() {
String elString="#order.money";
String elString2="#user";
String elString3="#p0";
TreeMap<String,Object> map=new TreeMap<>();
Order order = new Order();
order.setId(111L);
order.setMoney(123D);
map.put("order",order);
map.put("user","Hydra");
String val = parse(elString, map);
String val2 = parse(elString2, map);
String val3 = parse(elString3, map);
System.out.println(val);
System.out.println(val2);
System.out.println(val3);
}
至于Cache
相关参数的配置,我们沿用V1版本中的配置即可。准备工作做完了,下面我们定义切面,在切面中操作Cache
来读写Caffeine
的缓存,操作RedisTemplate
读写Redis
缓存。
@Configuration
@Aspect
public class DoubleCacheAop {
@Autowired
Cache<String,Object> caffeineCache;
@Autowired
RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.example.demo17.annotation.DoubleCache)")
public void doubleCachePointcut(){}
@Around("doubleCachePointcut()")
public Object cacheAround(ProceedingJoinPoint point) throws Throwable {
//获取切点方法签名
Signature signature = point.getSignature();
if (signature instanceof MethodSignature methodSignature){
//获取切点方法
Method method = methodSignature.getMethod();
//获取切点注解
DoubleCache annotation = method.getAnnotation(DoubleCache.class);
//注解属性
String key = annotation.key();
CacheType type = annotation.type();
//获取参数名
String[] parameterNames = methodSignature.getParameterNames();
//获取参数值
Object[] args = point.getArgs();
TreeMap<String, Object> treeMap = new TreeMap<>();
for (int i = 0; i < parameterNames.length; i++) {
treeMap.put(parameterNames[i],args[i]);
}
//入参解析
String parse = parse(key, treeMap);
String realKey = annotation.value() +":"+ parse;
if (type.equals(CacheType.DELETE)){
caffeineCache.invalidate(realKey);
redisTemplate.delete(realKey);
}else if (type.equals(CacheType.PUT)){
//执行代理方法
Object proceed = point.proceed();
caffeineCache.put(realKey,proceed);
redisTemplate.opsForValue().set(realKey,proceed);
return proceed;
}else {
Object ifPresent = caffeineCache.getIfPresent(realKey);
if (ifPresent == null){
Object o = redisTemplate.opsForValue().get(realKey);
if (o == null){
//执行代理方法
Object proceed = point.proceed();
caffeineCache.put(realKey,proceed);
redisTemplate.opsForValue().set(realKey,proceed);
return proceed;
}
caffeineCache.put(realKey,o);
return o;
}
return ifPresent;
}
}
//执行代理方法
return point.proceed();
}
public static String parse(String elString, TreeMap<String,Object> map){
elString=String.format("#{%s}",elString);
//创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
//通过evaluationContext.setVariable可以在上下文中设定变量。
EvaluationContext context = new StandardEvaluationContext();
map.entrySet().forEach(entry->
context.setVariable(entry.getKey(),entry.getValue())
);
//解析表达式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
//使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
String value = expression.getValue(context, String.class);
return value;
}
}
切面中主要做了下面几件工作:
-
通过方法的参数,解析注解中
key
的springEl
表达式,组装真正缓存的key
-
根据操作缓存的类型,分别处理存取、只存、删除缓存操作
-
删除和强制更新缓存的操作,都需要执行原方法,并进行相应的缓存删除或更新操作
-
存取操作前,先检查缓存中是否有数据,如果有则直接返回,没有则执行原方法,并将结果存入缓存
修改Service
层代码,代码中只保留原有业务代码,再添加上我们自定义的注解就可以了:
@Service
public class CacheService {
private HashMap<String, UserInfo> hashMap = new HashMap<>();
CacheService(){
hashMap.put("123",new UserInfo("123","张三"));
}
@DoubleCache(value = "user",key = "#userInfo.id",type = CacheType.PUT)
public UserInfo addUserInfo(UserInfo userInfo) {
hashMap.put(userInfo.getId(), userInfo);
return userInfo;
}
@DoubleCache(value = "user", key = "#id", type = CacheType.FULL)
public UserInfo getByName(String id) {
// 如果缓存中不存在,则从库中查找
UserInfo userInfo = hashMap.get(id);
if (userInfo == null){
return new UserInfo();
}
return userInfo;
}
}