首先我们先来了解一下springboot提供的缓存机制
Spring Boot 提供的缓存机制主要包括声明式和编程式两种方式
声明式
特点
- @EnableCaching 开启springboot缓存机制
- Spring Boot 默认的缓存库是 Simple Cache,它使用的是
ConcurrentMapCacheManager,其中缓存数据都存放在ConcurrentHashMap中。这意味着,如果没有特别指定使用其他缓存提供者,Spring Boot 会默认使用基于 Java 并发集合的缓存机制。这种缓存机制简单且轻量级,适用于不需要持久化缓存数据的场景- 自定义缓存库
- 导入依赖
- 建表
- 配置CacheManager缓存管理器
- 创建配置类:用于定义缓存的参数,如过期时间,序列化方式等
- 使用注解
- 自定义缓存注解
- 自定义缓存库
下面写个实例看看
- 依赖注入
<!-- web环境 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--缓存自动化配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--redis缓存库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.9</version>
</dependency>
<!-- 数据库驱动,这里以MySQL为例 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.23</version>
</dependency>
- 建表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户编号',
`username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '账号',
`password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`deleted` bit(1) DEFAULT NULL COMMENT '是否删除。0-未删除;1-删除',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
该demo项目使用的是springboot3.3.2的版本,使用的redis客户端是redisson
- 常用的redis客户端有
jedis,lettuce,redisson(jedis的增强版)
依赖导入
<!-- 实现对 Redisson客户端 的自动化配置 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.36.0</version>
</dependency>
<!--
jackson-datatype-jsr310 是 Jackson 提供的一个模块,用于支持 Java 8 的日期和时间 API (java.time 包)
的数据序列化和反序列化。Java 8 引入了新的日期时间类型(如 LocalDate, LocalDateTime, ZonedDateTime 等)
,这些类型不能直接被 Jackson 处理,需要使用这个模块来正确地进行 JSON 转换。
-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- 实现对 Caches 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
创建springCache的自动配置(声明式)
/**
* Cache 配置类,基于 Redis 实现
*/
@AutoConfiguration
@EnableConfigurationProperties({CacheProperties.class, CustomCacheProperties.class})
@EnableCaching
public class CacheAutoConfiguration {
/**
* RedisCacheConfiguration Bean
* <p>
* 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法
*/
@Bean
@Primary
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格
// 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客
// 再次修复单冒号,而不是双 :: 冒号问题,Issues 详情:https://gitee.com/zhijiantianya/yudao-cloud/issues/I86VY2
/**
* spring cache生成的key多出一个冒号
* computePrefixWith 方法:
*/
config = config.computePrefixWith(cacheName -> {
String keyPrefix = cacheProperties.getRedis().getKeyPrefix();
if (StringUtils.hasText(keyPrefix)) {
keyPrefix = keyPrefix.lastIndexOf(StrUtil.COLON) == -1 ? keyPrefix + StrUtil.COLON : keyPrefix;
return keyPrefix + cacheName + StrUtil.COLON;
}
return cacheName + StrUtil.COLON;
});
// 设置使用 JSON 序列化方式
RedisSerializationContext.SerializationPair<?> serializationPair =
RedisSerializationContext.SerializationPair.fromSerializer(RedisAutoConfiguration.buildRedisSerializer());
// 设置key 和 value 的序列化方式
config = config.serializeValuesWith(serializationPair);
// 设置 CacheProperties.Redis 的属性
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
// 设置缓存过期时间
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (!redisProperties.isCacheNullValues()) {
// 禁用缓存空值
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
// 禁用键前缀
config = config.disableKeyPrefix();
}
return config;
}
/**
* 配置Spring Date Redis 的缓存管理器
* @param redisTemplate redis模板
* @param redisCacheConfiguration
* @param CacheProperties
* @return
*/
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration,
CustomCacheProperties CacheProperties) {
// 创建 RedisCacheWriter 对象 : 该接口,用于定义缓存操作,例如获取、设置和删除缓存。
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(CacheProperties.getRedisScanBatchSize()));
// TimeoutRedisCacheManager 是 RedisCacheManager 的一个子类,用于添加缓存超时功能
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}
}
TimeoutRedisCacheManager
/**
* 支持自定义过期时间的 {@link RedisCacheManager} 实现类
*
* 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间。
* 单位为最后一个字母(支持的单位有:d 天,h 小时,m 分钟,s 秒),默认单位为 s 秒
*
* @author 芋道源码
*/
public class TimeoutRedisCacheManager extends RedisCacheManager {
private static final String SPLIT = "#";
public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
if (StrUtil.isEmpty(name)) {
return super.createRedisCache(name, cacheConfig);
}
// 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间
String[] names = StrUtil.splitToArray(name, SPLIT);
if (names.length != 2) {
return super.createRedisCache(name, cacheConfig);
}
// 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间
if (cacheConfig != null) {
// 移除 # 后面的 : 以及后面的内容,避免影响解析
String ttlStr = StrUtil.subBefore(names[1], StrUtil.COLON, false); // 获得 ttlStr 时间部分
names[1] = StrUtil.subAfter(names[1], ttlStr, false); // 移除掉 ttlStr 时间部分
// 解析时间
Duration duration = parseDuration(ttlStr);
cacheConfig = cacheConfig.entryTtl(duration);
}
// 创建 RedisCache 对象,需要忽略掉 ttlStr
return super.createRedisCache(names[0] + names[1], cacheConfig);
}
/**
* 解析过期时间 Duration
*
* @param ttlStr 过期时间字符串
* @return 过期时间 Duration
*/
private Duration parseDuration(String ttlStr) {
String timeUnit = StrUtil.subSuf(ttlStr, -1);
switch (timeUnit) {
case "d":
return Duration.ofDays(removeDurationSuffix(ttlStr));
case "h":
return Duration.ofHours(removeDurationSuffix(ttlStr));
case "m":
return Duration.ofMinutes(removeDurationSuffix(ttlStr));
case "s":
return Duration.ofSeconds(removeDurationSuffix(ttlStr));
default:
return Duration.ofSeconds(Long.parseLong(ttlStr));
}
}
/**
* 移除多余的后缀,返回具体的时间
*
* @param ttlStr 过期时间字符串
* @return 时间
*/
private Long removeDurationSuffix(String ttlStr) {
return NumberUtil.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1));
}
}
创建RedisAutoConfiguration(编程式)
/**
* Redis 配置类
*/
@AutoConfiguration(before = RedissonAutoConfigurationV2.class) // 目的:使用自己定义的 RedisTemplate Bean
public class RedisAutoConfiguration {
/**
* 创建 RedisTemplate Bean,使用 JSON 序列化方式
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。
template.setConnectionFactory(factory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(buildRedisSerializer());
template.setHashValueSerializer(buildRedisSerializer());
return template;
}
public static RedisSerializer<?> buildRedisSerializer() {
RedisSerializer<Object> json = RedisSerializer.json();
// 解决 LocalDateTime 的序列化
ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper");
objectMapper.registerModules(new JavaTimeModule());
return json;
}
}