1简介实例配置及依赖Redis配置业务代码测试
简介
说明
本文用示例介绍SpringBoot的缓存注解@Cacheable的用法。
本文重点展示@Cacheable的配置及其基础用法,详细用法见:SpringBoot缓存-注解的用法 – 自学精灵
示例介绍
需求:给分页接口加缓存,且设置其过期时间。
第1次访问时,真实请求,执行成功后@Cacheable注解会将结果缓存到Redis。
之后访问时,先从缓存中取,若缓存中有则直接从缓存中取,不再执行方法内的逻辑。
过期时间统一在配置类中设置,里边设置部分key的过期时间,其余的用默认的过期时间。
实例
配置及依赖
application.yml
spring:
redis:
host: 127.0.0.1
port: 6379
# password:
# database: 0 #指定数据库,默认为0
# timeout: 3000 #连接超时时间,单位毫秒,默认为0。也可以这么写:3s
# ssl: false # 是否启用SSL连接,默认false
# pool: #连接池配置
# max-active: 8 #最大活跃连接数,默认8个。
# max-idle: 8 #最大空闲连接数,默认8个。
# max-wait: -1 #获取连接的最大等待时间,默认-1,表示无限制,单位毫秒。
# #默认值可能会因为获取不到连接,导致事务无法提交,数据库被锁,大量线程处于等待状态的情况。
# min-idle: 0 #最小空闲连接数,默认0。
# sentinel:
# master: myMaster #哨兵master
# nodes: host1:port,host2:port #哨兵节点
# cluster:
# max-redirects: # 集群模式下,集群最大转发的数量
# nodes: host1:port,host2:port # 集群节点
pom.xml
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
Redis配置
常量
package com.example.demo.constant;
public class CachingConstant {
/**
* @Cacheable的cacheNames属性使用的值
*/
public interface CacheNames {
String CACHE_30_SECOND = "redis_cache_30_second";
String CACHE_1_MINUTE = "redis_cache_1_minute";
String CACHE_5_MINUTE = "redis_cache_5_minute";
String CACHE_10_MINUTE = "redis_cache_10_minute";
String CACHE_30_MINUTE = "redis_cache_30_minute";
String CACHE_1_HOUR = "redis_cache_1_hour";
String CACHE_2_HOUR = "redis_cache_2_hour";
String CACHE_6_HOUR = "redis_cache_6_hour";
String CACHE_12_HOUR = "redis_cache_12_hour";
String CACHE_1_DAY = "redis_cache_1_day";
String CACHE_15_DAY = "redis_cache_15_day";
String CACHE_30_DAY = "redis_cache_30_day";
String CACHE_60_DAY = "redis_cache_60_day";
String CACHE_180_DAY = "redis_cache_180_day";
String CACHE_365_DAY = "redis_cache_365_day";
}
}
配置
package com.example.demo.config;
import com.example.demo.constant.CachingConstant;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableCaching
public class RedisCachingConfig extends CachingConfigurerSupport {
/**
* 重写缓存Key生成策略。
* 包名+方法名+参数列表。防止缓存Key冲突
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
// 存放最终结果
StringBuilder resultStringBuilder = new StringBuilder("cache:key:");
// 执行方法所在的类
resultStringBuilder.append(target.getClass().getName()).append(".");
// 执行的方法名称
resultStringBuilder.append(method.getName()).append("(");
// 存放参数
StringBuilder paramStringBuilder = new StringBuilder();
for (Object param : params) {
if (param == null) {
paramStringBuilder.append("java.lang.Object[null],");
} else {
paramStringBuilder
.append(param.getClass().getName())
.append("[")
.append(String.valueOf(param))
.append("],");
}
}
if (StringUtils.hasText(paramStringBuilder.toString())) {
// 去掉最后的逗号
String trimLastComma = paramStringBuilder.substring(0, paramStringBuilder.length() - 1);
resultStringBuilder.append(trimLastComma);
}
return resultStringBuilder.append(")").toString();
};
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
Map
configurationMap.put(RedisConstant.CacheNames.CACHE_30_SECOND, createCacheConfig(Duration.ofSeconds(30)));
configurationMap.put(RedisConstant.CacheNames.CACHE_1_MINUTE, createCacheConfig(Duration.ofMinutes(1)));
configurationMap.put(RedisConstant.CacheNames.CACHE_5_MINUTE, createCacheConfig(Duration.ofMinutes(5)));
configurationMap.put(RedisConstant.CacheNames.CACHE_10_MINUTE, createCacheConfig(Duration.ofMinutes(10)));
configurationMap.put(RedisConstant.CacheNames.CACHE_30_MINUTE, createCacheConfig(Duration.ofMinutes(30)));
configurationMap.put(RedisConstant.CacheNames.CACHE_1_HOUR, createCacheConfig(Duration.ofHours(1)));
configurationMap.put(RedisConstant.CacheNames.CACHE_2_HOUR, createCacheConfig(Duration.ofHours(2)));
configurationMap.put(RedisConstant.CacheNames.CACHE_6_HOUR, createCacheConfig(Duration.ofHours(6)));
configurationMap.put(RedisConstant.CacheNames.CACHE_12_HOUR, createCacheConfig(Duration.ofHours(12)));
configurationMap.put(RedisConstant.CacheNames.CACHE_1_DAY, createCacheConfig(Duration.ofDays(1)));
configurationMap.put(RedisConstant.CacheNames.CACHE_15_DAY, createCacheConfig(Duration.ofDays(15)));
configurationMap.put(RedisConstant.CacheNames.CACHE_30_DAY, createCacheConfig(Duration.ofDays(30)));
configurationMap.put(RedisConstant.CacheNames.CACHE_60_DAY, createCacheConfig(Duration.ofDays(60)));
configurationMap.put(RedisConstant.CacheNames.CACHE_180_DAY, createCacheConfig(Duration.ofDays(180)));
configurationMap.put(RedisConstant.CacheNames.CACHE_365_DAY, createCacheConfig(Duration.ofDays(365)));
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(7))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.initialCacheNames(configurationMap.keySet())
.withInitialCacheConfigurations(configurationMap)
// 如果key不在configurationMap中,则使用此配置
.cacheDefaults(config)
.build();
}
@Bean
public RedisTemplate, ?> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate
template.setConnectionFactory(factory);
template.setKeySerializer(keySerializer());
template.setValueSerializer(valueSerializer());
template.setHashKeySerializer(keySerializer());
template.setHashValueSerializer(valueSerializer());
template.afterPropertiesSet();
return template;
}
private RedisSerializer
return new StringRedisSerializer();
}
private RedisSerializer
Jackson2JsonRedisSerializer
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 此项必须配置,否则如果序列化的对象里边还有对象,会报如下错误:
// java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
// 旧版写法:
// objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
private RedisCacheConfiguration createCacheConfig(Duration ttl) {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(ttl)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()));
}
}
业务代码
Controller
package com.example.demo.controller;
import com.example.demo.entity.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("page")
public Result page(int pageNo, int pageSize) {
return userService.page(pageNo, pageSize);
}
}
Service
接口
package com.example.demo.service;
import com.example.demo.entity.Result;
public interface UserService {
Result page(int pageNo, int pageSize);
}
实现
package com.example.demo.service.impl;
import com.example.demo.constant.RedisConstant;
import com.example.demo.entity.Result;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
private final List
new User(1L, "Tony1", 20),
new User(2L, "Tony2", 18),
new User(3L, "Tony3", 30),
new User(4L, "Tony4", 25),
new User(5L, "Tony5", 28)
);
@Override
@Cacheable(cacheNames = CachingConstant.CacheNames.CACHE_5_MINUTE)
public Result> page(int pageNo, int pageSize) {
String format = String.format("pageNo: %s, pageSize: %s", pageNo, pageSize);
System.out.println("从数据库中读数据。" + format);
int from = (pageNo - 1) * pageSize;
int to = Math.min(allUsers.size(), (pageNo) * pageSize);
List
return new Result>().data(users);
}
}
Entity
User
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
// 必须要有无参构造函数。因为Redis反序列化为对象时要用到
@NoArgsConstructor
public class User {
private Long id;
private String userName;
private Integer age;
}
Result
package com.example.demo.entity;
import lombok.Data;
@Data
public class Result
private boolean success = true;
private int code = 1000;
private String message;
private T data;
public Result() {
}
public Result(boolean success) {
this.success = success;
}
public Result
Result
if (success) {
result.code = 1000;
} else {
result.code = 1001;
}
return result;
}
public Result
return success(true);
}
public Result
return success(false);
}
/**
* @param code {@link ResultCode#getCode()}
*/
public Result
this.code = code;
return this;
}
public Result
this.message = message;
return this;
}
public Result
this.data = data;
return this;
}
}
测试
第1次请求
http://localhost:8080/user/page?pageNo=1&pageSize=2
postman结果:
后端结果:
从数据库中读数据。pageNo: 1, pageSize: 2
Redis结果:
第2次请求(重复第1次)
访问:http://localhost:8080/user/page?pageNo=1&pageSize=2
postman结果:
后端结果:无输出
Redis结果:(不会更新TTL)
第3次请求(使用新参数)
访问:http://localhost:8080/user/page?pageNo=2&pageSize=2
postman结果:
后端结果:
从数据库中读数据。pageNo: 2, pageSize: 2
Redis结果:(方法返回值存入Redis,与其他的key的超时时间是分开的)
本次请求的结果:
之前请求的结果: