【@Cacheable】一个注解实现方法返回结果的Redis缓存(带过期时间)
- 0、 前言
- 1、 @Cacheable注解基本原理
- 2、 基于Redis的方法返回结果缓存Demo
- 2.1、 依赖
- 2.2、 Spring配置文件
- 2.3、 配置类
- 2.3、 实体类
- 2.4、 服务层
- 2.5、 控制层
- 3、 测试
- 3.1 测试1
- 3.2 测试2
0、 前言
本文主要对SpringBoot框架中的@Cacheable注解的使用进行介绍,由于这个注解是不支持设置过期时间的,可能会导致内存溢出,为解决该问题,本文使用Redis结合@Cacheable将方法返回结果进行缓存。
若仅仅使用Spring自身的缓存,可以参考:Spring 缓存在项目中的使用
1、 @Cacheable注解基本原理
@Cacheable是Spring框架中用于支持缓存功能的注解。它可以应用于方法级别,用于指示Spring在调用方法之前检查缓存,并在缓存中找到结果时直接返回,而不必实际执行方法
。在使用时候,要在启动类上加上"@EnableCaching"注解,如下:
@EnableCaching
@SpringBootApplication
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class, args);
}
}
@Cacheable注解有以下几个主要的参数:
更详细的参数讲解,参考:@Cacheable注解属性介绍
- value:指定使用哪个缓存管理器来管理缓存,默认情况下为"default"。
- cacheNames:指定要使用的具体缓存名称,可以同时指定多个,逗号分隔。例如:“cacheA, cacheB”。
- key:指定生成缓存key的策略(SpEL表达式),默认使用全部方法参数作为key。例如,如果只希望使用第一个参数作为key,则可以设置为"#param1"。
- condition:指定一个SpEL表达式,仅当满足条件时才进行缓存。例如:"#result != null"表示只有当方法返回值不为空时才进行缓存。
- unless:与condition相反,在满足条件时不进行缓存。例如:"#result == null"表示只有当方法返回值为空时才进行缓存。
- keyGenerator:指定自定义的键生成器类,用于生成自定义的缓存key。如果未指定,则使用默认值。
- cacheManager:指定自定义的缓存管理器类,用于处理特定的缓存操作。如果未指定,则使用默认值。
以下是一个示例代码:
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
// 从数据库或其他数据源获取用户信息
return userRepository.findById(userId);
}
}
在上述示例中,getUserById()方法会首先检查名为"users"的缓存中是否存在以userId作为key的结果,如果存在则直接返回该结果;否则会执行从数据库中获取用户信息,并将结果放入缓存供下次调用时使用。
这样,在后续对同一用户id进行getUserById()方法调用时就无需再次访问数据库,而是直接从缓存中获取数据。
2、 基于Redis的方法返回结果缓存Demo
整个demo的工程目录结构如下:
2.1、 依赖
<!--引入 redis starter 的maven依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring boot 配置处理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
2.2、 Spring配置文件
# application.properties
spring.redis.host=192.168.1.222
spring.redis.port=6379
spring.redis.password=xxx
spring.redis.database=0
2.3、 配置类
- Redis配置类
package com.whut.cache.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置hashKey 和 hashValue序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
- 带过期时间的RedisManager
在这个
package com.whut.cache.config;
import lombok.NonNull;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.StringUtils;
import java.time.Duration;
public class TtlRedisCacheManager extends RedisCacheManager {
public final static Long DEFAULT_REDIS_CACHE_TTL = 60L;
public TtlRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(@NonNull String name, RedisCacheConfiguration cacheConfig) {
// 这里的name,就是@Cacheable注解的 value或者cacheNames值
String[] cells = StringUtils.delimitedListToStringArray(name, "=");
String redisKey = cells[0];
if (cells.length > 1) {
// 如果有设置,比如@Cacheable (value = "user=30", key = "#id"), 那么30就是过期时间
long ttl = Long.parseLong(cells[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}else{
// 默认1分钟
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(DEFAULT_REDIS_CACHE_TTL));
}
return super.createRedisCache(redisKey, cacheConfig);
}
}
- 缓存配置类
package com.whut.cache.config;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.lang.reflect.Method;
import java.util.Objects;
@Configuration
public class CacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
ArrayList<String> keyList = new ArrayList<>();
keyList.add(target.getClass().getName());
keyList.add(method.getName());
for (Object param : params) {
if (param != null) {
keyList.add(String.valueOf(JSON.toJSONString(param).hashCode()));
}
}
return String.join(":",keyList);
}
};
}
/**
* 自定义RedisCacheManager,用于使用@Cacheable时设置 缓存过期时间ttl
*/
@Bean(value = "redisCacheManager")
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return new TtlRedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
2.3、 实体类
package com.whut.cache.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 7458531607966442378L;
private Integer id;
private String name;
private Integer age;
private String sex;
}
请求DTO
@Data
public class UserDto {
private Integer id;
private String name;
private Integer age;
private String sex;
}
2.4、 服务层
接口类
package com.whut.cache.service;
import com.whut.cache.domain.User;
public interface UserService {
User getUserById(Integer id);
User saveUser(UserDto request);
}
实现类
package com.whut.cache.service.impl;
import com.whut.cache.config.TtlRedisCacheManager;
import com.whut.cache.domain.User;
import com.whut.cache.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
@Service
public class UserServiceImpl implements UserService {
private final static String USER_CACHE_PREFIX = "UserCache";
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private RedisCacheManager cacheManager;
@Override
@Cacheable(value = USER_CACHE_PREFIX + "=120", key = "#id", cacheManager = "redisCacheManager")
public User getUserById(Integer id) {
// 1. 模拟查数据库
ArrayList<User> userData = new ArrayList<>();
userData.add(new User(1, "xiaoming", 32, "boy"));
userData.add(new User(2, "xiaohong", 18, "girl"));
userData.add(new User(3, "whut", 22, "boy"));
System.out.println("查询数据库中id = " + id + "的用户" );
/*
// 如果有需要,也可以缓存一些中间结果:
Cache user = cacheManager.getCache(USER_CACHE_PREFIX + "::" + id);
user.put("test",1111);
*/
// 2. 根据id查数据库
return userData.stream()
.filter(item -> id.equals(item.getId()))
.findFirst().orElse(null);
}
@Override
@Cacheable(value = USER_CACHE_PREFIX + "=120",keyGenerator = "myKeyGenerator", cacheManager = "redisCacheManager")
public User saveUser(UserDto request) {
User user = new User();
BeanUtils.copyProperties(request, user);
System.out.println(user);
return user;
}
}
2.5、 控制层
@RestController
public class CacheControlelr {
@Resource
private UserService userService;
@PostMapping("/getUserById")
public ResponseHelper getUserById(@RequestParam Integer id) {
return ResponseHelper.ok(userService.getUserById(id));
}
@PostMapping("/saveUser")
public ResponseHelper saveUser(@RequestBody UserDto request) {
userService.saveUser(request);
return ResponseHelper.ok("保存成功", null);
}
}
工具类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseHelper<T> {
public String msg;
public T result;
public String succ;
public static <T>ResponseHelper<T> ok(){
return new ResponseHelper<>(null, null, "ok");
}
public static <T>ResponseHelper<T> ok(T data){
return new ResponseHelper<>(null, data, "ok");
}
public static <T>ResponseHelper<T> ok(String msg, T data){
return new ResponseHelper<>(msg, data, "ok");
}
public static <T> ResponseHelper<T> fail(String msg) {
return new ResponseHelper<>(msg, null, "fail");
}
}
3、 测试
3.1 测试1
第一次访问 http://localhost:8080/getUserById?id=3
控制台打印信息:
redis中数据:
再次访问http://localhost:8080/getUserById?id=3, 控制台无打印信息,说明@Cacheable
生效,直接从缓存中读取数据。
3.2 测试2
第一次访问http://localhost:8080/saveUser
控制台有打印信息:
User(id=1, name=god, age=24, sex=boy)
redis中有保存相关信息
第二次在访问时候,控制台无打印信息,仍然有信息返回,说明是从缓存中取的信息。