所谓缓存,即 Redis 会把 MySQL 中经常被查询的数据缓存起来,这样当用户来访问的时候,就不需要到 MySQL 中去查询了,而是直接获取 Redis 中的缓存数据,从而降低了后端数据库的读取压力。
如果说用户查询的数据 Redis
没有,此时用户的查询请求就会转到 MySQL
数据库,当 MySQL
将数据返回给客户端时,同时会将数据缓存到 Redis
中,这样用户再次读取时,就可以直接从 Redis
中获取数据。
明白了实现原理,下面就介绍如何在 Spring Boot
项目中配置 Redis
缓存。
一、注解介绍
1. 作用描述
在具体上手之前,我们先了解一下 Spring Boot
中缓存常用的注解。
注解 | 作用 |
---|---|
@CacheConfig | 作用于实现缓存方法的类上,通过 cacheNames 设置统一前缀。 |
@Cacheable | 执行方法前会根据 Key 读取缓存,若存在则返回缓存并跳过方法执行,否则继续执行方法并将结果存入缓存。 |
@CachePut | 将每次方法返回值存入缓存中,但与 @Cacheable 不同是其在方法执行之前不会读取缓存。 |
@CacheEvict | 根据 Key 删除对应缓存,通常在对数据变更之后需要更新缓存。 |
@Caching | 通过 @Caching 可实现多种缓存嵌套,如同时进行缓存写入和删除。 |
2. 示例介绍
在上面表格中详细介绍了几类常见的注解与其对应的作用,下面通过具体的示例加深了解。
@CacheConfig(cacheNames = "users")
public class UserServiceImpl implements UserService {
@Cacheable(key = "#id")
public User get(String id) {
return userDao.query(id);
}
@CachePut(key = "#id")
public User query(String id) {
return userDao.query(id);
}
@CacheEvict(key = "#user.id")
public User update(User user) {
return userDao.update(user);
}
@Caching(
cacheable = {
@Cacheable(key = "'list'"),
@Cacheable(key = "#user.id")
},
evict = {
@CacheEvict(key = "'list'"),
@CacheEvict(key = "#user.id")
})
public User insert(User user) {
return userDao.insert(user);
}
}
二、项目配置
了解了缓存注解的基本使用,下面就介绍在现实业务场景中如何进行使用。
1. 项目创建
Spring Boot项目创建步骤之前文章已经详细讲过了,这里不再重复介绍,之后文章将基于之前的工程进行改造,具体步骤参考之前文章:Spring boot实现接口开发。
创建完项目后在 Maven
工程中引入 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>
2. 文件配置
在application.yml
文件中新增 Redis 相关配置,其中password
是可选的,如果你没有设置密码就注释该行。
spring:
redis:
host: localhost
port: 6379
password: 123456
lettuce:
pool:
max-idle: 8
max-active: 8
max-wait: -1ms
min-idle: 0
shutdown-timeout: 100ms
3. Redis配置
通过自定义配置对 Redis 缓存过期时间、键值对序列化进行设置,新建RedisConfig
文件,其中key
、values
序列化使存入缓存的数据以可读的方式存储。
注意下面两点:
如果设置了
disableCachingNullValues()
禁止缓存空值,在使用@Cacheable
时需要添加unless="#result == null"
,否则当查询到空值时程序将会出现异常。如果不设置禁止空值,即空值也进行存储,虽然不会对数据库产生性能损耗,但会浪费
Redis
缓存空间。因此需要根据具体情况进行判断选择。
@Configuration
public class RedisConfig {
/**
* 配置缓存管理器
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存前缀
.prefixCacheNameWith("healthcare:")
// 设置缓存过期时间
.entryTtl(Duration.ofMinutes(5))
// 设置 key 序列化
.serializeKeysWith(keyPair())
// 设置 value 序列化
.serializeValuesWith(valuePair());
Map<String, RedisCacheConfiguration> redisMap = new HashMap<>();
redisMap.put("users", redisCacheConfiguration);
// 返回 Redis 缓存管理器
return RedisCacheManager.builder(factory)
.withInitialCacheConfigurations(redisMap)
.build();
}
/**
* 配置 键 序列化
*/
private RedisSerializationContext.SerializationPair<String> keyPair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer());
}
/**
* 配置 值 序列化
*
* 使用 GenericJackson2JsonRedisSerializer 替换默认序列化
*/
private RedisSerializationContext.SerializationPair<Object> valuePair() {
return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
}
}
三、业务实操
1. 开启缓存
在启动类上添加 @EnableCaching
开启注解。
@EnableCaching
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在 Service
实现类及方法上添加 @CacheConfig
并配置缓存前缀,注意要和上面配置类中设置的一致。
@Service
@CacheConfig(cacheNames = "users")
public class UserServicesImpl implements UserServices {
// do something
}
在需要开启缓存的方法前添加 @Cacheable
注解并设置相关参数。
- 其中
key = "#ID"
表示用传入的ID
作为缓存的键值,如传入123
则在Redis
中存储路径为users:123
。 - 如果在配置文件中设置了禁止空值,需要在
@Cacheable
中添加unless="#result == null"
,否则当查询到空值时程序将会出现异常。
@Service
@CacheConfig(cacheNames = "users")
public class UserServicesImpl implements UserServices {
@Cacheable(key = "#ID")
public User get(String ID) {
return userMapper.get(ID);
}
@Cacheable(key = "#user.ID")
public int update(User user) {
return userMapper.update(user);
}
@Cacheable(key = "#ID", unless="#result == null")
public User get(String ID) {
return userMapper.get(ID);
}
}
此时通过Postman
调用接口发送重复请求后端查询语句只会执行一次,而不会像之前重复的请求数据库,从而实现性能优化。
2. 缓存删除
我们先看一下它是如何将数据存入缓存的,通过 @Cacheable
注解在获取数据之后通过配置的KEY
值将查询的数据以键值对的形式存入缓存。
@Cacheable(key = "'list'")
public List<User> list() {
return userMapper.list();
}
那么问题就简单了,若要解决数据不一致的情况,只需在更新操作的时候同时将缓存进行删除。当下次再发送请求,由于缓存中没有所需数据,将会重新转到后端数据库请求,此时查询到的数据即为最新数据。
实现起来也很简单,通过 @CacheEvict
注解配置对应的key
即可。
@CacheEvict(key = "'list'")
public int add(User user) {
return userMapper.insert(user);
}
3. 多缓存删除
当删除单个缓存值时通过 @CacheEvict
注解可以直接删除,但如果删除多个缓存时就需要使用 @Caching
进行缓存清除了。
举个简单例子,比如用户表一共有两条数据,项目在针对获取单个用户数据和全部表中数据都做了缓存处理,代码如下:
@Cacheable(key = "'list'")
public List<User> list() {
return userMapper.list();
}
@Cacheable(key = "#user.id")
public User getUser(User user) {
return userMapper.getUser(user);
}
此时对表中 id = 123
的数据进行了更新,显然缓存中的 users:list
和 users:123
数据和真实数据已经不一致了,但 @CacheEvict
只能删除单条缓存。
这时候我们就需要@Caching
组合多个注解进行缓存删除,代码实现如下:
@Caching(evict = {
@CacheEvict(key = "'list'"),
@CacheEvict(key = "#user.id")
}
)
public int update(User user) {
return userMapper.update(user);
}
还有一种更粗暴的方式,只要数据发生变更就直接清空所有缓存,但实际开发中用到相对较少,因为会将正常的缓存一同清空。
@CacheEvict(allEntries = true)
public int update(User user) {
return userMsapper.update(user);
}
示例源码:Redis缓存配置