Haoyuan's blog Haoyuan's blog
首页
导航站
  • Java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • Spring Cloud
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
留言区
GitHub (opens new window)

Somnus Haoyuan

Word is cheap, show me the code.
首页
导航站
  • Java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • Spring Cloud
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
留言区
GitHub (opens new window)
  • 设计模式

  • 算法

  • 知识

    • 知识 - 对象
    • 知识 - 幂等性
    • 知识 - 分布式事务
    • 知识 - MapStruct
    • 知识 - MapStructPlus
    • 知识 - CompletableFuture
    • 知识 - EasyExcel
    • 知识 - Encrypt
    • 知识 - 接口幂等性
    • 知识 - 数据脱敏
    • 知识 - WebSocket
    • 知识 - Spring Cache
      • 前言
      • 实现
        • 缓存实现
        • 缓存管理
        • 配置项
        • 容器装配
      • 示例
    • 知识 - 请求日志输出
    • 知识 - 接口限流
  • 开发
  • 知识
Haoyuan
2024-06-15
目录

知识 - Spring Cache

  • 前言
  • 实现
    • 缓存实现
    • 缓存管理
    • 配置项
    • 容器装配
  • 示例

# 前言

本内容介绍 Spring Cache 的自定义实现缓存功能,如果只是使用 Spring Cache 的内置缓存功能或者了解 Spring Cache API,请看 Spring - Cache

Spring Cache 内置了很多缓存方案,但是有场景需要既有本地缓存,也有 Redis 缓存。将热点数据缓存到本地,这样获取可以快速返回,然后其他数据缓存到 Redis,需要时去 Redis 获取。

下面介绍如何实现 Caffeine + Redis 缓存方案,Caffeine 作为一级缓存,Redis 作为二级缓存。

实现步骤:

  1. 自定义类实现 Cache 接口。该接口是 Spring Cache 提供的自定义缓存类,比如 Spring Cache 内置了 RedisCache、EhCache、JCache、CaffeineCache 等缓存类,这些缓存的实现也是实现了 Cache 接口,重写里面的方法。

    • Cache 接口需要实现的方法都是对缓存数据的增删改查,也就是说,缓存的增删改查,都是通过 Cache 接口来实现的,因此我们自定义缓存类,就必须实现 Cache,这样 Spring 就会调用实现了这些增删改查方法的自定义类。

    • 自定义类具体看 CaffeineRedisCache 类。实现增删改查的方法里,调用了 Caffeine 和 RedisCache 的增删改查方法。就形成了 Spring Cache 调用 Cache 的增删改查方法 -> 调用 Caffeine 和 RedisCache 的增删改查方法。实现一级和二级缓存

  2. 自定义类实现 CacheManager 接口。该接口是 Spring Cache 提供的自定义缓存管理类,比如 Spring Cache 内置了 Redis、EhCache、JCache、Caffeine 等缓存管理类,这些缓存管理类的实现也是实现了 CacheManager 接口,重写里面的方法。

    • CacheManager 接口需要实现的方法就是获取 Cache 缓存类,比如 RRedisCache、EhCache、JCache、CaffeineCache 等。

    • 自定义类具体看 CaffeineRedisCacheManager 类。在该类里,获取了 RedisCacheManager,获取该 Manager 的目的是获取 RedisCache,因为 RedisCache 内置了对 Redis 的缓存操作。然后将 RedisCache 传入自定义缓存类 CaffeineRedisCache 里,实现缓存增删改查

当我们使用 Cacheable、CachePut、CacheEvict 等注解后,最终会会调用 CacheManager 的 getCache 方法,从而获取 Cache 类,最后调用 Cache 类的增删改查缓存操作。

# 实现

依赖:

<dependencies>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.32</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
  </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 缓存实现

Caffeine + Redis 实现双重缓存

@RequiredArgsConstructor
public class CaffeineRedisCache implements Cache {

    // 从 CacheConfiguration 获取缓存实例
    private static final com.github.benmanes.caffeine.cache.Cache<Object, Object> CAFFEINE = SpringHelper.getBean("caffeine");

    private final Cache cache;

    public String getUniqueKey(Object key) {
        return cache.getName() + ":" + key;
    }

    @Override
    public String getName() {
        return cache.getName();
    }

    @Override
    public Object getNativeCache() {
        return cache.getNativeCache();
    }

    @Override
    public ValueWrapper get(Object key) {
        // 从 Caffeine 获取缓存,如果缓存不存在,则从 cache 获取,并缓存到 Caffeine
        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key));
        return (ValueWrapper) o;
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        // 从 Caffeine 获取缓存,如果缓存不存在,则从 cache 获取,并缓存到 Caffeine
        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, type));
        return (T) o;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        Object o = CAFFEINE.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
        return (T) o;
    }

    @Override
    public void put(Object key, Object value) {
        // 在 Caffeine 删除缓存,不能存储 value,否则取出来类型转换报错
        CAFFEINE.invalidate(getUniqueKey(key));
        cache.put(key, value);
    }

    @Override
    public void evict(Object key) {
        // 在 cache 删除缓存
        boolean b = cache.evictIfPresent(key);
        if (b) {
            // 在 Caffeine 删除缓存
            CAFFEINE.invalidate(getUniqueKey(key));
        }
    }

    @Override
    public void clear() {
        cache.clear();
        CAFFEINE.invalidateAll();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

# 缓存管理

Caffeine + Redis 双重缓存管理

@RequiredArgsConstructor
public class CaffeineRedisCacheManager implements CacheManager {
    Map<String, Cache> cacheMap = new ConcurrentHashMap<>();

    private final RedisConnectionFactory redisConnectionFactory;
    private final CacheProperties cacheProperties;

    @Override
    public Cache getCache(String cacheName) {
        // 重写 cacheName 支持多参数。如:#cacheName#ttl
        String[] array = StringUtils.delimitedListToStringArray(cacheName, "#");
        cacheName = array[0];

        Cache cache = cacheMap.get(cacheName);
        if (Objects.nonNull(cache)) {
            return cache;
        }

        // Redis 缓存配置项
        RedisCacheConfiguration configuration = createConfiguration(cacheProperties);

        // cacheName 后面的参数默认是 ttl
        if (array.length > 1) {
            long seconds = DurationStyle.detectAndParse(array[1]).toSeconds();
            configuration.entryTtl(Duration.ofSeconds(seconds));
        }

        // 创建 RedisCacheManager,从里面获取 Redis 的缓存类
        RedisCacheManager redisCacheManager = new RedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), configuration);

        // 获取 RedisCache,里面只想 Redis 封装了 Redis 的命令操作
        Cache redisCache = redisCacheManager.getCache(cacheName);

        CaffeineRedisCache newCache = new CaffeineRedisCache(redisCache);
        cacheMap.put(cacheName, newCache);
        return newCache;
    }

    /**
     * 参考 Spring 实现 Redis Cache 的配置项:{@link org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration#createConfiguration}
     *
     * @param cacheProperties Spring Cache 缓存配置项
     */
    private RedisCacheConfiguration createConfiguration(CacheProperties cacheProperties) {
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        RedisCacheConfiguration config = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(getJacksonSerializer()));

        if (Objects.nonNull(redisProperties.getTimeToLive())) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }else {
            // 默认 2 小时
            config = config.entryTtl(Duration.ofHours(2));
        }
        if (Objects.nonNull(redisProperties.getKeyPrefix())) {
            config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;
    }

    /**
     * 获取 Jackson 序列化器,序列化到 Redis 的值,方法参考 {@link cn.youngkbt.redis.config.RedisTemplateConfig#initRedisTemplate(RedisTemplate)}
     * @return Jackson 序列化器
     */
    private Jackson2JsonRedisSerializer<Object> getJacksonSerializer() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);

        // 解决 Redis 无法存入 LocalDateTime 等 JDK8 的时间类
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        /*
         * 新增 LocalDateTime 序列化、反序列化规则
         */
        javaTimeModule
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DatePatternPlus.NORM_DATETIME_FORMATTER)) // yyyy-MM-dd HH:mm:ss
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME)) // HH:mm:ss
                .addSerializer(Instant.class, InstantSerializer.INSTANCE) // Instant 类型序列化
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DatePatternPlus.NORM_DATETIME_FORMATTER)) // yyyy-MM-dd HH:mm:ss
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ISO_LOCAL_DATE)) // yyyy-MM-dd
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ISO_LOCAL_TIME)) // HH:mm:ss
                .addDeserializer(Instant.class, InstantDeserializer.INSTANT);// Instant 反序列化

        objectMapper.registerModules(javaTimeModule);

        // 使用 Jackson2JsonRedisSerialize 替换默认序列化
        return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
    }

    @Override
    public Collection<String> getCacheNames() {
        return Collections.unmodifiableSet(cacheMap.keySet());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

# 配置项

Spring Boot Application 配置项

@Data
@ConfigurationProperties("spring.cache.caffeine")
public class CaffeineCacheProperties {
    /**
     * 缓存过期时间,单位秒
     */
    private Duration expire = Duration.ofSeconds(30);
    
    /**
     * 缓存初始化容量
     */
    private int capacity = 100;
    
    /**
     * 缓存最大容量
     */
    private Long maximumSize = 1000L;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 容器装配

@EnableCaching
@AutoConfiguration(after = RedisConnectionFactory.class)
@EnableConfigurationProperties({CaffeineCacheProperties.class, CacheProperties.class})
public class CacheConfiguration {
    /**
     * caffeine 本地缓存处理器
     */
    @Bean
    public Cache<Object, Object> caffeine(CaffeineCacheProperties properties) {
        return Caffeine.newBuilder()
                // 过期时间
                .expireAfterWrite(properties.getExpire())
                // 初始的缓存空间大小
                .initialCapacity(properties.getCapacity())
                // 缓存的最大条数
                .maximumSize(properties.getMaximumSize())
                .build();
    }

    /**
     * 自定义缓存管理器 整合spring-cache
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties properties) {
        return new CaffeineRedisCacheManager(redisConnectionFactory, properties);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

Spring Boot 3.x 需要在 resource 下建立 META-INF/spring 路径,然后创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为

cn.youngkbt.cache.config.CacheConfiguration
1

这样 Spring 会自动扫描该文件的容器装配类,将里面涉及的类注入到 Spring 容器。

# 示例

首先在 application 里配置 Spring Cahce 时,不要使用 type,然后正常使用 Spring Cache 提供的注解即可,缓存的逻辑将使用上面实现的缓存逻辑。

编辑此页 (opens new window)
#knowledge
更新时间: 2025/04/06, 01:04:59
知识 - WebSocket
知识 - 请求日志输出

← 知识 - WebSocket 知识 - 请求日志输出→

最近更新
01
技术随笔 - Element Plus 修改包名 原创
11-02
02
Reactor - 扩展性
11-02
03
Reactor - 最佳实践
11-02
更多文章>
Theme by Vdoing | Copyright © 2021-2025 Somnus Haoyuan | MIT License
京公网安备 11010802034042号 京ICP备2025120692号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式