当请求的数量达到一定的规模,这时候频繁的数据库请求就算使用数据库集群也很难承受的了,这时候可以将一些信息存于缓存当中,避免请求直接大量的怼到数据库上。而如何实现缓存的策略是使用数据库 + 缓存当中的至关重要的一步,Spring Boot 对缓存的支持也是很方便的。
Redis
和 DataTemplate 一样对数据库打开关闭操作的简化,对于 Redis 的打开关闭,有着 RedisTemplate 类,结合 RedisConnectionFactory(实现用 JedisConnectionFactory) 用于处理一些打开关闭连接的繁琐工作。
首先添加 redis 的 starter 和其 jedis client:
1
2
3
4
5
6
7
8
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
|
可以使用 RedisTemplate 和泛型,或者直接使用 StringRedisTemplate,这需要根据实际存储的 Redis 数据而决定,这里会将数据序列化为 JSON 格式,所以使用 RedisTemplate<String, String>
泛型,新建一个 config.RedisConfig.java
配置类:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Configuration
public class RedisConfig {
@Bean
public JedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory());
return template;
}
}
|
由于是配置类,上面的 redisTemplate 里面的调用 redisConnectionFactory 的方法调用会被 Spring 拦截然后注入,详情见 配置类细则。Redis 的配置在 application.properties 即可配置 redis 的连接信息,简单的示例如下:
1
2
3
|
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.timeout=30000
|
之后就可以在其地方注入使用,如这里简单的将请求里的 kv 存到 redis 当中:
1
2
3
4
5
6
7
8
9
10
11
12
|
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
RedisTemplate<String, String> redisTemplate;
@PostMapping("/set")
public String setKV(@RequestParam String key, @RequestParam String val) {
redisTemplate.opsForValue().set(key, val);
return redisTemplate.opsForValue().get(key);
}
}
|
然后就可以在 redis-cli 终端中进行下面的测试:
1
2
3
4
5
6
7
8
9
10
11
|
$ redis-cli
redis 127.0.0.1:6379> MONITOR
OK
1547536551.338767 "MONITOR"
# 新终端
$ curl -X POST 'http://localhost:8080/redis/set/?key=name&val=Jack'
# redis-cli 终端
1547536657.984896 "SET" "name" "Jack"
1547536657.985396 "GET" "name"
|
除了 opsForValue
之外,还有下列的操作:
1
2
3
4
5
6
|
redisTemplate.opsForGeo(); //操作地址位置
redisTemplate.opsForValue(); //操作字符串
redisTemplate.opsForHash(); //操作 hash
redisTemplate.opsForList(); //操作 list
redisTemplate.opsForSet(); //操作 set
redisTemplate.opsForZSet(); //操作有序 set
|
缓存注解
直接使用 redisTemplate 操作缓存,虽然简化了 Redis 的连接和关闭,但是在实际过程中还是需要编写大量的业务代码。这时候可以使用 缓存注解 + RESTful 风格接口 来简化。
在使用 Spring 缓存注解之前需要使用配置缓存管理器 CacheManager,缓存管理器将提供如缓存类型,在上面的配置类里面相关 bean,并为了后续的对象的缓存修改下 RedisTemplate 的序列化方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(redisTemplate().getValueSerializer()));
return RedisCacheManager.builder(redisConnectionFactory()).cacheDefaults(redisCacheConfiguration).build();
}
@Bean
public JedisConnectionFactory redisConnectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
// 也可用 bean
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisJsonSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
// 默认是 JdkSerializationRedisSerializer
redisTemplate.setValueSerializer(genericJackson2JsonRedisJsonSerializer);
// 照样可以设置其他的 serializer
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisJsonSerializer);
return redisTemplate;
}
}
|
上面的代码的解释如下:
- 相比 SpringBoot 1 的利用 RedisTemplate (对于序列化这样设置 CacheManager 时候会方便很多)初始化 RedisCacheManager,在 SpringBoot 2 中 变更 为使用 RedisConnectionFactory,如果想要设置 RedisCacheManager 需要使用 RedisCacheManager.builder,也可在配置文件里配置。SpringBoot 1 的配置方式 参考。
@EnableCaching
注解能使缓存机制。
上面的代码其实是有问题的,因为自己指定了 redisConnectionFactory ,这样的方式是使用的默认配置(localhost)没有用 properties 里面的配置,所以要想能够使用配置有两种方法,一是自己讲配置里面的值注入到自己的 redisConnectionFactory()
函数里的 JedisConnectionFactory 的构造当中。而是使用 data-redis 的 JedisConnectionFactory。
这里使用第二种方法试试,就需要去掉 redisConnectionFactory Bean,然后加入 autowire 的 JedisConnectionFactory redisConnectionFactory;
之后重启会发现 Bean method 'redisConnectionFactory' in 'JedisConnectionConfiguration' not loaded because @ConditionalOnMissingBean
错误,查看到源码之后会发现 org.springframework.boot.autoconfigure.data.redis.JedisConnectionConfiguration#redisConnectionFactory
的上面有一个注解 @ConditionalOnMissingBean({RedisConnectionFactory.class})
,这样说明已有 RedisConnectionFactory 的存在,所以改为注入 RedisConnectionFactory 即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@Configuration
@EnableCaching
public class RedisConfig {
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public CacheManager cacheManager() {
// ...
return RedisCacheManager.builder(redisConnectionFactory.cacheDefaults(redisCacheConfiguration).build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
// ...
}
}
|
开发 service,注意要为 User POJO 加上 implements Serializable
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@Service
public class MybatisUserServiceImpl implements UserService {
@Override
@CachePut(value = "redisCache", key = "'user_'+#user.id")
public User insertUser(User user) {
userMapper.createUser(user);
return user;
}
@Override
@Cacheable(value = "redisCache", key = "'user_'+#id")
public User getUser(int id) {
return userMapper.getUserById(id);
}
@Override
@CachePut(value = "redisCache", condition = "#result != 'null'", key = "'user_'+#user.id")
public User updateUser(User user) {
userMapper.updateUser(user);
return user;
}
@Override
@CacheEvict(value = "redisCache", key = "'user_'+#id", beforeInvocation = false)
public int deleteUser(int id) {
return userMapper.deleteUser(id);
}
// 批量获取缓存命中率低,忽略
}
|
最后可以请求测试一下,比如请求两次已有数据,创建一个数据之后请求一次,在 redis 中是这样的:
1
2
3
4
5
6
7
8
9
|
# 这里是第一次访问,Redis 缓存该方法返回值
1547550684.227003 "GET" "redisCache::user_1"
1547550684.235508 "SET" "redisCache::user_1" "{\"@class\":\"contacts.model.User\",\"id\":1,\"name\":\"Jack\",\"address\":\"London\"}"
# 第二次访问,直接使用缓存
1547550689.227003 "GET" "redisCache::user_1"
# CachePut 时候缓存
1547550690.958848 "SET" "redisCache::user_44" "{\"@class\":\"contacts.model.User\",\"id\":44,\"name\":\"Scott\",\"address\":\"Beijing\"}"
# 再次访问时直接获取
1547550693.902872 "GET" "redisCache::user_44"
|
对于上面代码的解释:
@CachePut
注解将方法返回数据放入缓存。
@Cacheable
注解先根据指定的键查询缓存,如存在直接将缓存反序列化后返回,如不存在,再缓存方法返回值。
@CacheEvict
通过定义的键移除缓存。
可以看到上面的 Redis 的键格式如 redisCache::user_X
其中的 redisCache 即是使用 value 参数指定的,value 支持数组;后面的 user_X 即是 key 指定的,key 里面是可以使用 SpEL 表达式 的,condition 的 SpEL 表达式可以指定操作缓存的条件。
由于采用了 JSON 方式的序列化,所以缓存中的序列化数据可读性还是较强的。
更多参考 Cache Abstraction。
对于设置过期时间可以在 cacheManager 里统一设置,配置里的 spring.cache.redis.time-to-live
好像没有作用?
1
2
3
4
5
6
7
8
9
10
11
|
public class RedisConfig {
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(redisTemplate().getValueSerializer()))
.entryTtl(Duration.ofSeconds(60));
return RedisCacheManager.builder(redisConnectionFactory()).cacheDefaults(redisCacheConfiguration).build();
}
// ...
}
|
也可以使用 @TimeToLive
注解 POJO 类的一个返回该类存活时间的方法或者属性来差异化存活,具体参考 Time To Live。
CacheConfig
在上面的例子中一个 service 当中多个缓存注解都干了同一个事情,即指定 cacheName,对于一个 service 内的缓存一般来说其 cacheName 都是一致的,这时候就可以使用 @CacheConfig
注解来装饰类级别,这样方法内的缓存注解就不用指明缓存名了:
1
2
3
4
5
|
@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
@Cacheable
public String getAddress(Customer customer) {...}
}
|
Caching
在 java 中是不允许同一个注解装饰重复使用的,但是利用 @Caching
注解就可实现多个缓存的注解:
1
2
3
4
|
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}
|
缓存 Client
如果追求更加精细的缓存操作,可以自己再封装一层 RedisClient 然后自己在手动调用,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Component
public class RedisClient {
@Autowired
private RedisTemplate redisTemplate;
public <T> T get(String key) {
return (T)redisTemplate.opsForValue().get(key);
}
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void set(String key, Object value, int timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
public void expire(String key, int timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
|
Lettuce VS Jedis
Jedis 在实现上是直接连接的 redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个 Jedis 实例增加物理连接
Lettuce 的连接是基于 Netty 的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为 StatefulRedisConnection 是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
所以可在后续开发中使用 Lettuce 逐步替换掉 Jedis。
参考