本地缓存Caffeine

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开销,降低这一过程中在网络通信上的耗时

缺点:但不支持大数据量存储、数据更新时不好保证各节点数据一致性、数据随应用进程的重启而丢失

优点:

  1. 高性能:Caffeine的设计目标就是提供高性能的缓存解决方案。它采用了高效的算法和数据结构,使得缓存的读写操作都非常快速,从而能够显著减少对数据库的访问,降低数据库的压力。

  2. 灵活性:Caffeine支持多种缓存策略,如基于容量、基于时间、基于权重、手动移除、定时刷新等,并提供了丰富的配置选项,可以根据不同的应用场景和需求进行灵活配置。

  3. 内存友好:Caffeine在缓存管理方面非常注重内存的使用效率,它采用了近乎最佳的命中率算法,以减少不必要的内存占用,并提供了内存使用情况的统计和监控功能,有助于优化缓存的使用。

  4. 易于集成:Caffeine可以与多种Java框架和库进行无缝集成,如Spring等,使得在项目中引入和使用Caffeine变得非常简单和方便。

缺点:

  1. 存储容量有限:由于Caffeine是本地缓存,它的存储容量受限于应用进程的内存空间。因此,对于需要存储大量数据的应用场景,Caffeine可能无法满足需求。

  2. 可靠性较低:本地缓存的数据随应用进程的重启而丢失,这意味着如果应用进程崩溃或重启,缓存中的数据将会丢失。这可能导致数据的不一致性和可靠性问题。

  3. 无法共享(不适用于分布式系统共享 ) :本地缓存只支持被该应用进程访问,一般无法被其他应用进程访问。因此,在需要多个进程或服务器之间共享缓存数据的场景下,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)引用回收

AsyncCacheAsyncLoadingCache 缓存不支持软引用和弱引用。

  • 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;
    }
}

切面中主要做了下面几件工作:

  • 通过方法的参数,解析注解中keyspringEl表达式,组装真正缓存的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;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值