概述
在商业中 “现金为王”,而在互联网整个软件世界中,有一个与之相近的说法是“缓存为王”。
本文我们重点说明缓存方向:将方法的返回值缓存起来,下次再调用该方法时,如果方法的参数与之前调用时的参数相同,则会直接返回缓存中的结果,而不会再执行方法体。这样可以提高方法的执行效率
优点
- 提高性能:缓存可以将方法的结果存储在内存中,后续对相同参数的方法调用可以直接从缓存中获取结果,避免重复计算,提高方法的执行效率。
- 减轻数据库压力:对于需要频繁访问数据库的方法,可以将结果缓存在内存中,减轻数据库的压力,提高数据库的响应速度。
- 简化代码逻辑:通过使用缓存,可以避免编写复杂的手动缓存代码或条件判断逻辑,使代码更简洁、可读性更好。
缺点
- 内存占用:缓存的数据存储在内存中,因此如果缓存的数据量过大,会消耗大量的内存资源。在使用缓存时需要合理安排系统的内存和缓存容量。
- 数据一致性:使用缓存后,需要注意维护数据的一致性。当数据发生变化时,需要及时更新缓存,以避免脏数据的问题。可通过合理设计缓存策略和使用缓存失效机制来解决这个问题。
- 缓存失效:缓存的有效期限需要合理设置。如果缓存的有效期太长,可能导致数据更新不及时;如果缓存的有效期太短,可能增加重复执行代码的次数。需要根据具体业务场景来选择合适的缓存有效期。
- 缓存击穿:当某个缓存条目在缓存失效时,同时有大量的并发请求到达时,可能会导致缓存击穿问题,即大量请求直接击穿到数据库。可以通过加锁或使用分布式锁等机制来避免缓存击穿。
注解介绍
Spring缓存机制通过 @EnableCaching开启,配合 @Cacheable、 @CachePut、 @CacheEvict等注解,为Java应用提供了一种声明式管理缓存的方式。这些注解使得缓存配置变得简洁明了,允许开发者轻松实现数据的自动缓存、更新和清除,从而优化应用性能,减少不必要的计算和数据访问开销。
1. 启用缓存支持
@EnableCaching
注解用于在Spring配置类上启用缓存管理功能。
- 注解属性介绍
无属性。
- 注解业务案例
1. @Configuration
2. @EnableCaching
3. public class CacheConfig {
4. // 配置其他Bean`
5. }
2. 缓存结果
@Cacheable
注解用于声明一个方法的结果是可缓存的。
-
注解属性介绍
-
value
或cacheNames
: 指定缓存名称,可以是单个或多个。 -
key
: 指定缓存键的SpEL表达式。 -
condition
: 指定缓存的条件SpEL表达式。 -
unless
: 指定不缓存的条件SpEL表达式。 -
注解业务案例 单一缓存名称和键:
1. @Cacheable("books")
2. public Book findBookById(String id) {
3. // 业务逻辑
4. }
多个缓存名称和条件:
1. @Cacheable(value = {"books", "archivedBooks"}, condition = "#id.length() > 10")
2. public Book findBookWithComplexKey(String id) {
3. // 业务逻辑
4. }
6. @Service
7. public class MyService {
9. /**
10. `* 一个使用 @Cacheable 所有属性的案例。`
11. `*`
12. `* @param id 用户ID`
13. `* @return 返回用户对象`
14. `*/
15. @Cacheable(
16. value = "users", // 缓存名称`
17. key = "#id", // 缓存键,使用SpEL表达式`
18. condition = "#id.length() > 3", // 缓存条件,只有当ID长度大于3时才缓存`
19. unless = "#result == null" // 除非条件,如果结果为null,则不缓存`
20. )
21. public User findUserById(String id) {
22. // 模拟数据库查询操作,这里假设ID长度小于3时没有结果`
23. if (id == null || id.length() <= 3) {
24. return null;
25. }
26. return performDatabaseQuery(id);
27. }
29. private User performDatabaseQuery(String id) {
30. // 模拟数据库查询逻辑`
31. return new User(id, "Name based on ID");
32. }
33. }
3. 更新缓存
@CachePut
注解用于在方法执行后更新缓存。
- 注解属性介绍
与 @Cacheable
相同。
- 注解业务案例
1. @CachePut("books")
2. public Book updateBookDetails(String id, Book details) {
3. // 业务逻辑
4. }
5.
6. @Service
7. public class MyService {
8.
9. /**
10. * 使用 @CachePut 所有属性的案例。
11. *
12. * @param user 用户对象,包含ID
13. * @return 更新后的用户对象
14. */
15. @CachePut(
16. value = "users", // 缓存名称
17. key = "#user.id", // 缓存键,使用SpEL表达式
18. condition = "#user.age > 18" // 条件,只有当用户年龄大于18时才更新缓存
19. )
20. public User updateUserProfile(User user) {
21. // 模拟更新用户信息的业务逻辑
22. return performUpdate(user);
23. }
24.
25. private User performUpdate(User user) {
26. // 模拟更新逻辑
27. user.setName("Updated Name");
28. return user;
29. }
30. }
4. 清除缓存
@CacheEvict
注解用于在方法执行后清除缓存。
注解属性介绍
-
value
或cacheNames
: 指定缓存名称,可以是单个或多个。 -
allEntries
: 清除所有缓存项。 -
condition
: 指定清除缓存的条件SpEL表达式。
注解业务案例
清除特定缓存名称的条目:
1. @CacheEvict("books")
2. public void deleteBook(String id) {
3. // 业务逻辑
4. }
清除所有缓存名称的所有条目:
1. @CacheEvict(allEntries = true)
2. public void clearAllCaches() {
3. // 业务逻辑
4. }
5. 组合缓存操作
@Caching
注解用于组合多个缓存操作。
- 注解属性介绍
value
: 包含多个缓存操作的数组。
- 注解业务案例
1. @Caching(
2. cacheable = {@Cacheable("books")},
3. put = {@CachePut("books")},
4. evict = {@CacheEvict("archivedBooks")}
5. )
6. public Book processBook(String id, Book details) {
7. // 业务逻辑
8. }
代码演示
redis依赖安装
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.21</version>
</dependency>
- redis配置文件
spring:
redis:
# 连接地址
host: "127.0.0.1"
# 端口
port: 6379
# 数据库
database: 1
password: helloworld
# 连接超时
connect-timeout: 5s
# 读超时
timeout: 5s
# Lettuce 客户端的配置
lettuce:
# 连接池配置
pool:
# 最小空闲连接
min-idle: 0
# 最大空闲连接
max-idle: 8
# 最大活跃连接
max-active: 8
# 从连接池获取连接 最大超时时间,小于等于0则表示不会超时
max-wait: -1ms
接口缓存配置类
框架给我们提供了 @Cacheable 注解用于缓存方法返回内容。但是 @Cacheable 注解 不能 定义缓存 有效期
。这样的话在一些需要自定义缓存有效期的场景就不太实用。
框架给我们提供的 RedisCacheManager
类,是 准对这一块的 就配置类, 我们可以根据此类,自己实现 缓存有效期 的功能。
package com.xuzhongkj.configrations;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.Objects;
@Configuration
@EnableCaching
public class CustomRedisCacheManager {
private String keyPreFix = "whero";
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
private RedisSerializer<Object> valueSerializer() {
// fastjson 中的类
return new GenericFastJsonRedisSerializer();
}
/*
* @description 配置自定义 缓存管理器: RedisCacheManager
**/
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
// 缓存注解 配置:
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//设置 key 为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
//设置 value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
// 使用 prefixCacheNameWith 需要注意系统自动拼接的双”:“问题
.computePrefixWith(cacheName -> keyPreFix + ":" + cacheName + ":");
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
RedisCacheManager redisCacheManager = new CustomRedisCacheManagerToolsClass(redisCacheWriter, config);
return redisCacheManager;
}
}
/**
* @Title: 自定义redis缓存管理器, 为了实现 缓存有效期的 动态性
*/
class CustomRedisCacheManagerToolsClass extends RedisCacheManager {
public static final String SEPARATOR = "#";
public CustomRedisCacheManagerToolsClass(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(@NotNull(message = "缓存名称不能为空") String name, RedisCacheConfiguration cacheConfiguration) {
// 举例说明: name = "bookCourse:getCourseId#7D"
if (name.contains(SEPARATOR)) {
String[] spelStr = name.split(SEPARATOR);
String key = spelStr[0];
String valueStr = spelStr[1];
int length = valueStr.length();
if (length >= 2) {
String cycleTimeStr = valueStr.substring(0, length - 1);
if (cycleTimeStr.matches("\\d+")) {
long cycleTime = Long.parseLong(cycleTimeStr);
String cycleUnit = valueStr.substring(length - 1, length);
if (cycleUnit.equals("D")) {//表示天
return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofDays(cycleTime)));
}
if (cycleUnit.equals("H")) {//表示小时
return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofHours(cycleTime)));
}
if (cycleUnit.equals("M")) {//表示分钟
return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofMinutes(cycleTime)));
}
if (cycleUnit.equals("S")) {//表示秒
return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofSeconds(cycleTime)));
} else {
// 都不是则使用默认配置
return super.createRedisCache(name, cacheConfiguration);
}
}
}
}
return super.createRedisCache(name, cacheConfiguration);
}
}
这里面简单对 RedisCacheConfiguration 缓存配置做一下说明:
- serializeKeysWith():设置 Redis 的 key 的序列化规则。
- erializeValuesWith():设置 Redis 的 value 的序列化规则。
- computePrefixWith():计算 Redis 的 key 前缀。
- cacheConfiguration.entryTtl(): 设置 @Cacheable 注解缓存的有效期。
测试使用
service层
package com.xuzhongkj.services.impls;
import com.xuzhongkj.pojo.Userswtt;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class WttService {
@Cacheable(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
public Userswtt getName (String name) {
System.out.println("------- 创建 -----------"+name);
Userswtt u = new Userswtt();
u.setOpenid("qwejlksdfjkdf");
u.setAge(11);
u.setAvatar("https:/asdfj;askdf");
u.setUid(123L);
return u;
}
@CachePut(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
public Userswtt EdtName (String name) {
System.out.println("---------- 更新 --------"+name);
Userswtt u = new Userswtt();
u.setOpenid("qwejlksdfjkdf");
u.setAge(22);
u.setAvatar("https:/asdfj;askdf");
u.setUid(123L);
return u;
}
@CacheEvict(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
public Userswtt DelName (String name) {
System.out.println("--------- 删除 ---------"+name);
Userswtt u = new Userswtt();
return u;
}
}
controller 层
@RestController
@Slf4j
@RequestMapping("/test")
public class WttTest {
@Autowired
private WttService wttService;
@GetMapping("/wtt")
public Userswtt a1() {
Userswtt name = wttService.getName("testResObj");
return name;
}
@GetMapping("/wtt2")
public Userswtt a2() {
Userswtt name = wttService.EdtName("testResObj");
return name;
}
@GetMapping("/wtt3")
public Userswtt a3() {
return wttService.DelName("testResObj");
}
redis中键值对的存储情况:
- key:
whero:bookCourse:getCourseId:testResObj
- value
{“@type”:“com.xuzhongkj.pojo.Userswtt”,“age”:11,“avatar”:“https:/asdfj;askdf”,“openid”:“qwejlksdfjkdf”,“uid”:123}