简介
说明
本文用示例介绍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
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>demo_Cacheable_SpringBoot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>demo_Cacheable_SpringBoot</name> <description>demo_Cacheable_SpringBoot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</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> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
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<String, RedisCacheConfiguration> configurationMap = new HashMap<>(); 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<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(keySerializer()); template.setValueSerializer(valueSerializer()); template.setHashKeySerializer(keySerializer()); template.setHashValueSerializer(valueSerializer()); template.afterPropertiesSet(); return template; } private RedisSerializer<String> keySerializer() { return new StringRedisSerializer(); } private RedisSerializer<Object> valueSerializer() { Jackson2JsonRedisSerializer<Object> 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<User> allUsers = Arrays.asList( 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<List<User>> 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<User> users = new ArrayList<>(allUsers.subList(from, to)); return new Result<List<User>>().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<T> { 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<T> success(boolean success) { Result<T> result = new Result<>(success); if (success) { result.code = 1000; } else { result.code = 1001; } return result; } public Result<T> success() { return success(true); } public Result<T> failure() { return success(false); } /** * @param code {@link ResultCode#getCode()} */ public Result<T> code(int code) { this.code = code; return this; } public Result<T> message(String message) { this.message = message; return this; } public Result<T> data(T data) { 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的超时时间是分开的)
本次请求的结果:
之前请求的结果:
请先
!