0%

Redis在SpringBoot中的实践

Redis,目前非常流行的内存数据库,其广泛应用于Web场景的缓存技术下。本文简要介绍在SpringBoot下的Redis的实践应用

abstract

配置Redis

1. 添加Redis依赖

在pom.xml中添加Redis依赖

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<!-- 如果在配置文件中显式配置redis客户端,则需添加连接池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.1</version>
</dependency>

2. 添加Redis服务器配置

在application.properties配置文件中添加Redis服务器参数配置。

1
2
3
4
5
6
7
8
9
# Redis 服务器配置
# Redis 数据库索引
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=127.0.0.1
# Redis 服务器连端口
spring.redis.port=6379
# Redis 服务器连接密码
spring.redis.password=123456

3. 添加Redis客户端配置

Spring Boot从2.X版本开始引入新的客户端——Lettuce。较之前的Jedis相比而言,Lettuce是一个基于Netty的线程安全的Redis客户端。故,我们这里推荐使用Lettuce,并在application.properties配置文件中对Lettuce客户端进行配置

[Note]:

  • 配置文件中的Redis客户端配置可以不写,则Spring Boot(2.X及以上版本)会默认使用Lettuce客户端
  • 当我们在配置文件显式地配置Redis客户端时,需要添加commons-pool2连接池依赖
1
2
3
4
5
6
7
8
9
# Redis客户端Lettuce配置
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0

4. 编写Java Config类

  • 自定义RedisTemplate Bean用于直接操作Reids,而不使用SpringBoot默认提供的,故该Bean实例名称必须要为redisTemplate,这样才可以避免Spring生成默认的RedisTemplate实例。对于Redis中的Key使用String序列化,而对于Value则使用更通用的Json序列化,而不是默认的JDK序列化
  • 自定义Spring Cache的缓存管理器CacheManager,配置其使用redis数据库中间件,这样我们即可利用Spring Cache的注解快速操作缓存的查询和删除,而不是直接去使用redisTemplate来访问redis数据库

[Note]:

  • 值得一提的是,这里虽然为了方便将RedisTemplate的配置类和CacheManager的配置类写在了一起,但是显然两者是两个相对独立的东西,Spring Cache是对各种缓存中间件(包含但不止于Redis)的抽象,通过注解统一缓存的操作方式,而Redis则是一种具体的缓存实现方案
  • 需要在配置类上添加@EnableCaching注解,以使能Spring Cache
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j
@Configuration
@EnableCaching // 使能Spring Cache缓存
public class RedisConfig extends CachingConfigurerSupport{

// Key 过期时间: 1day = 86400s
private Duration timeToLive = Duration.ofDays(1);

// Spring Cache 配置类
@Bean(name="cacheManager")
public CacheManager cacheManager(RedisConnectionFactory factory){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl( timeToLive ) // 设置缓存的过期时间
.computePrefixWith(cacheName -> cacheName + ":") // 无该行代码,则Spring Cache 默认使用::用作命名空间的分隔符
.serializeKeysWith( RedisSerializationContext.SerializationPair.fromSerializer( getKeySerializer() ) ) // 设置Key序列化器
.serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer( getValueSerializer() ) ) // 设置Value序列化器
.disableCachingNullValues();

RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults( redisCacheConfiguration )
.build();
log.info(" 自定义Spring Cache Manager配置完成 ... ");
return redisCacheManager;
}

// Redis 配置类
// 自定义的RedisTemplate的Bean名称必须为 redisTemplate。当方法名不为 redisTemplate时,可通过name显示指定bean名称,@Bean(name="redisTemplate")
@Bean(name = "redisTemplate")
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置String Key序列化器
template.setKeySerializer( getKeySerializer() );
template.setValueSerializer( getValueSerializer() );
// 设置Hash Key序列化器
template.setHashKeySerializer( getKeySerializer() );
template.setHashValueSerializer( getValueSerializer() );
log.info("自定义RedisTemplate配置完成 ... ");
return template;
}

// key 采用String序列化器
private RedisSerializer<String> getKeySerializer() {
return new StringRedisSerializer();
}

// value 采用Json序列化器
private RedisSerializer<Object> getValueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}

缓存操作

直接使用RedisTemplate操作缓存

1. 封装RedisTemplate操作

将redisTemplate常用方法封装到RedisService中,便于我们后面使用

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/**
* Redis 操作类
*/
@Service
public class RedisService {

@Autowired
private RedisTemplate redisTemplate;

/*----------------------- key 基本操作 ----------------------------*/

/**
* 判断 key 是否存在
* @param key
* @return
*/
public Boolean existKey(String key) {
return redisTemplate.hasKey(key);
}

/**
* 删除 key
* @param key
*/
public Boolean deleteKey(String key) {
return redisTemplate.delete(key);
}

/**
* 获取 key-value 的 数据类型
* @param key
* @return
*/
public DataType typeKey(String key) {
return redisTemplate.type(key);
}

/**
* 设置key的过期时间
* @param key
* @param timeout
* @param unit
* @return
*/
public Boolean setExpire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}

/**
* 获取 key 的剩余过期时间
* @param key
* @param unit
* @return
*/
public Long getExpire(String key, TimeUnit unit){
return redisTemplate.getExpire(key, unit);
}

/**
* 获取 key 的剩余过期时间
* @param key
* @return
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key);
}

/**
* 移除 key 的过期时间,该 key 将永久存在
* @param key
* @return
*/
public Boolean persistExpire(String key) {
return redisTemplate.persist(key);
}

/**
* 将 key 移动至给定index编号的数据库中
* @param key
* @param dbIndex
* @return
*/
public Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}

/*----------------------- String key 操作 ----------------------------*/

/**
* 设置String key的值
* @param key
* @param value
*/
public void setStringKey(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}

/**
* 获取String key的值
* @param key
* @return
*/
public Object getStringKey(String key) {
return redisTemplate.opsForValue().get(key);
}

/*----------------------- Hash key 操作 ----------------------------*/

/**
* 设置Hash Key中指定字段的值
* @param key
* @param field
* @param value
*/
public void setHashKey(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}

/**
* 获取Hash Key中指定字段的值
* @param key
* @param field
* @return
*/
public Object getHashKey(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}

/**
* 获取Hash Key全部字段的值
* @param key
* @return
*/
public Map<Object, Object> getHashKeyAll(String key) {
return redisTemplate.opsForHash().entries(key);
}

/**
* 判断Hash Key中指定字段是否存在
* @param key
* @param field
* @return
*/
public Boolean existField(String key, String field) {
return redisTemplate.opsForHash().hasKey(key, field);
}

/**
* 根据方法名和Map参数生成Hash Key的filed字段值
* @param methodName
* @param map
* @return
*/
public String getField(String methodName, Map map){
StringBuilder sb = new StringBuilder(methodName);
if (!MapUtil.isEmpty(map)) {
sb.append(JSONUtil.parseObj(map));
}
return sb.toString();
}
}

2. 编写测试用例

这里分别提供了一个String Key的写、读缓存测试用例,验证我们的Redis缓存服务是否设置成功

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
@Controller
@ResponseBody
@RequestMapping(value = "testRedis")
public class TestRedisController {

@Autowired
private RedisService redisService;

/*------------------------- Test String Key -------------------------------*/
@GetMapping("/testSetStringKey/{id}")
public String testSetStringKey(@PathVariable Integer id) {
Student stu = new Student();
stu.setId(id);
stu.setSex("女:" + id);
stu.setUsername("Amy");
// 写入缓存
redisService.setStringKey("testStringKey-" + id, stu);
return "缓存写入成功";
}

@GetMapping("/testGetStringKey/{id}")
public Student testGetStringKey(@PathVariable Integer id) {
// 读取缓存
Student stu = (Student)redisService.getStringKey("testStringKey-" + id);
return stu;
}
}

3. 测试

在Redis控制台中执行 keys * 命令,查看Redis中当前存在Key

figure 1.jpeg

http://localhost:8080/testRedis/testSetStringKey/1234 发送请求以实现向Redis中写入缓存

figure 2.jpeg

看看Redis数据库是否发生了变化,从下图可以看到,新加入了一个名为testStringKey-1234的String Key,其值为Student对象Json序列化后的字符串,此时,说明缓存写入成功

figure 3.jpeg

http://localhost:8080/testRedis/testGetStringKey/1234 发送请求以实现从Redis中读取相应的缓存数据,下图结果表明缓存读取成功

figure 4.jpeg

通过管道Pipeline批量操作

Reids会对每条命令建立一个连接来进行操作。故对于大量命令来说,建议通过管道Pipeline来一次性发送多条命令。此举可以大幅度提高效率。避免频繁建立连接引起的时间消耗。这里我们以批量操作String类型的数据为例进行说明。代码如下所示

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
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 基于管道 Pipeline 的批量操作
*/
@Service
public class PipelineService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 批量设置String key的值
* @param map key-value对
* @param seconds TTL, Unit: s
*/
public void setStringKeyByPipeline(Map map, Long seconds) {
RedisSerializer keySerializer = redisTemplate.getKeySerializer();
RedisSerializer valueSerializer = redisTemplate.getValueSerializer();

RedisCallback redisCallback = connection -> {
map.forEach( (key, value) ->
connection.set(keySerializer.serialize(key), valueSerializer.serialize(value), Expiration.seconds(seconds), RedisStringCommands.SetOption.UPSERT)
);
return null;
};

redisTemplate.executePipelined(redisCallback);
}

/**
* 批量获取String key的值
* @param list key list
* @param <E>
* @return
*/
public <E> List<E> getStringKeyByPipeline(List<String> list) {
RedisSerializer keySerializer = redisTemplate.getKeySerializer();

RedisCallback redisCallback = connection -> {
list.forEach( key -> connection.get(keySerializer.serialize(key)) );
return null;
};

List<E> result = redisTemplate.executePipelined(redisCallback);
return result;
}

}

可以看到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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RunWith(SpringRunner.class)
@SpringBootTest
public class PipelineTest {

@Autowired
private PipelineService pipelineService;

/**
* 测试: 通过管道批量设置
*/
@Test
public void test1() {
// 一天的秒数
Long ttl = 24*3600L;
Integer num = 1000*1000;
Map map = new HashMap( (int)Math.ceil(num/0.75) );

Long start = System.currentTimeMillis();
for (Integer i=0; i<num; i++) {
String name = "Msg" + i;
Msg msg = new Msg(name, i);
map.put(name, msg);
}
pipelineService.setStringKeyByPipeline(map, ttl);
Long end = System.currentTimeMillis();

double time = (end-start) / 1000;
System.out.println("Time: " + time);

}

/**
* 测试: 通过管道批量读取
*/
@Test
public void test2() {
Integer num = 1000*1000;
List<String> keyList = new LinkedList<>();

Long start = System.currentTimeMillis();
for (Integer i=0; i<num; i++) {
String name = "Msg" + i;
keyList.add(name);
}
List<Msg> valueList = pipelineService.getStringKeyByPipeline(keyList);
Long end = System.currentTimeMillis();

double time = (end-start) / 1000;
System.out.println("Time: " + time);

}

@AllArgsConstructor
@Data
@NoArgsConstructor
public static class Msg {
private String name;
private Integer code;
}

}

通过Sping Cache注解操作缓存

@Cacheable注解

该注解可以标记在方法、类上,表明该方法是支持缓存的。当调用该方法时,Spring Cache会首先检查该方法对应的缓存。如果缓存中该Key存在,则直接将缓存Key中的Value作为方法的结果返回,而无需进入并执行方法;如果缓存中无指定Key,则进入并执行方法,在将返回值返回的同时将其存入缓存中,以便下次调用该方法时,直接从缓存中获取方法的执行结果,而无需再进入方法执行

可用属性:

  • value: 缓存名称。在Redis中其实际上即为命名空间。默认使用::作为命名空间的分隔符,上文的RedisConfig配置类的cacheManager()方法中,自定义:作为Redis的命名空间
  • key: 缓存Key名。自定义缓存Key名中可以使用“#参数”方式引用形参中的值
  • unless: 方法返回值不存入缓存的条件, 使用#result引用方法的返回值

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@ResponseBody
@RequestMapping("testSpringCache")
public class TestSpringCacheController {

private static final String cacheName = "SpringCacheTest";

@RequestMapping(value = "/testCacheable")
@Cacheable(value = cacheName, key = "#name+'-'+#age", unless = "#result == null")
public User testCacheable(@RequestParam String name, @RequestParam Integer age) {
User user = new User();
user.setName(name);
user.setAge(age);
return user;
}
}

Redis数据库中的情况

figure 5.png

我们向 http://localhost:8080/testSpringCache/testCacheable?name=Aaron&age=18 发送请求后,再查看此时Redis数据库中的情况,即可看到多了一个Key为SpringCacheTest:Aaron-18的缓存,如上所述,将SpringCacheTest作为命名空间的名称,使用:作为命名空间的分隔符,而该缓存的过期时间TTL也为上文配置的RedisConfig.timeToLive值

figure 6.jpeg

@CacheEvict注解

该注解可以标记在方法、类上,表明调用该方法是即清除指定缓存。当调用该方法时,Spring Cache即会清除指定的缓存

可用属性:

  • value: 指定欲清除key所在的缓存名称(命名空间名)
  • key: 指定欲清除的缓存Key名
  • allEntries: 是否清除指定命名空间下的所有key的缓存,默认为false。当其为true时,将忽略key属性配置指定key
  • beforeInvocation: 是否在调用方法前去清除指定缓存。当其为true时,将会在方法执行前清除缓存;当其为false时,将会在方法成功执行后清除缓存,如果方法未成功执行(抛出异常)将无法清除缓存

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
@ResponseBody
@RequestMapping("testSpringCache")
public class TestSpringCacheController {

private static final String cacheName = "SpringCacheTest";

@RequestMapping(value = "/testOneCacheEvict")
@CacheEvict(value = cacheName, key="'Tony'",beforeInvocation = true)
public void testOneCacheEvict() {
System.out.println("清除cacheNamem[SpringCacheTest]命名空间下的key[Tony]缓存");
return;
}

@RequestMapping(value = "/testAllCacheEvict")
@CacheEvict(value = cacheName, allEntries = true,beforeInvocation = true)
public void testAllCacheEvict() {
System.out.println("清除cacheNamem[SpringCacheTest]命名空间下的所有缓存");
return;
}
}

Redis数据库中的情况:

figure 7.png

我们向 http://localhost:8080/testSpringCache/testOneCacheEvict 发送请求后,再查看此时Redis数据库中的情况,即可发现命名空间SpringCacheTest下key为Tony的缓存已经被成功清除

figure 8.png

我们再向 http://localhost:8080/testSpringCache/testAllCacheEvict 发送请求,再查看此时Redis数据库中的情况,即可发现命名空间SpringCacheTest下所有key的缓存全部被清除完毕

figure 9.png

[Note]:

  1. allEntries为true时,只能清除命名空间value下所有key的缓存,并不会清除缓存名恰好为命名空间value的缓存,如上图所示,调用testAllCacheEvict()方法后,SpringCacheTest缓存依然保留,不会被清除。欲清除该缓存,可通过redisTemplate操作
  2. Redis下key的TTL(剩余生存时间)含义如下:
  • -2:表示该key不存在
  • -1:表示该key存在,但未设置剩余生存时间。即永久存在
  • >=0:对于不小于0的TTL值,表示该key的剩余生存时间,Unit:s。当为0时,即被自动删除
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝