Spring Boot 整合 Redis 实现缓存功能

Posted by 彭楷淳 on 2021-01-23
Estimated Reading Time 9 Minutes
Words 2.3k In Total
Viewed Times

使用 Java 操作 Redis 的方案很多,Jedis 是目前较为流行的一种方案,除了 Jedis,还有很多其他解决方案,如下:

img

除了这些方案之外,还有一个使用也相当多的方案,就是 Spring Data Redis。在传统的 SSM 中,需要开发者自己来配置 Spring Data Redis,这个配置比较繁琐,主要配置 3 个东西:连接池、连接器信息以及 key 和 value 的序列化方案。

在 Spring Boot 中,默认集成的 Redis 就是 Spring Data Redis,默认底层的连接池使用了 lettuce,开发者可以自行修改为自己的熟悉的,例如 Jedis

Spring Data Redis 针对 Redis 提供了非常方便的操作模板 RedisTemplate。这是 Spring Data 擅长的事情,那么接下来我们就来看看 Spring Boot 中 Spring Data Redis 的具体用法。

创建项目


引入依赖

在 pom.xml 中主要引入 rediscommos-pool2 的依赖,完整的 pom.xml 依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>

相关配置

接下来配置 Redis 的信息,信息包含两方面,一方面是 Redis 的基本信息,另一方面则是连接池信息:

1
2
3
4
5
6
7
8
9
spring.redis.database=0
spring.redis.password=123
spring.redis.port=6379
spring.redis.host=192.168.66.128
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=1ms
spring.redis.lettuce.shutdown-timeout=100ms

原理分析


当开发者在项目中引入了 Spring Data Redis,并且配置了 Redis 的基本信息,此时,自动化配置就会生效。我们从 Spring Boot 中 Redis 的自动化配置类中就可以看出端倪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

这个自动化配置类很好理解:

  1. 首先标记这个是一个配置类,同时该配置在 RedisOperations 存在的情况下才会生效(即项目中引入了 Spring Data Redis
  2. 然后导入在 application.properties 中配置的属性
  3. 然后再导入连接池信息(如果存在的话)
  4. 最后,提供了两个 Bean ,RedisTemplateStringRedisTemplate,其中 StringRedisTemplateRedisTemplate 的子类,两个的方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate 中的两个泛型都是 Object ,意味者存储的 key 和 value 都可以是一个对象,而 StringRedisTemplate 的 两个泛型都是 String ,意味者 StringRedisTemplate 的 key 和 value 都只能是字符串。如果开发者没有提供相关的 Bean ,这两个配置就会生效,否则不会生效。

基本使用


这里只简单实现对 Redis 的增删改查操作

RedisService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface RedisService {

/**
* 存储缓存
* @param key
* @param value
* @param seconds
*/
void set(String key, Object value, long seconds);

/**
* 获取缓存
* @param key
* @return
*/
Object get(String key);

/**
* 删除缓存
* @param key
*/
boolean del(String key);
}

RedisServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class RedisServiceImpl implements RedisService {

@Autowired
private RedisTemplate redisTemplate;

@Override
public void set(String key, Object value, long seconds) {
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
}

@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}

@Override
public boolean del(String key) {
return redisTemplate.delete(key);
}
}

接下来,我们来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class HelloService {

@Autowired
RedisService redisService;

public void hello() {
redisService.set("k1", "v1");
Object k1 = redisService.get("k1");
System.out.println(k1);
}
}

Redis 中的数据操作,大体上来说,可以分为两种:

  1. 针对 key 的操作,相关的方法就在 RedisTemplate
  2. 针对具体数据类型的操作,相关的方法需要首先获取对应的数据类型,获取相应数据类型的操作方法是 opsForXXX

调用该方法就可以将数据存储到 Redis 中去了,如下:

img

k1 前面的字符是由于使用了 RedisTemplate 导致的,RedisTemplate 对 key 进行序列化之后的结果。RedisTemplate 中,key 默认的序列化方案是 JdkSerializationRedisSerializer。而在 StringRedisTemplate 中,key 默认的序列化方案是 StringRedisSerializer,因此,如果使用 StringRedisTemplate,默认情况下 key 前面不会有前缀。不过开发者也可以自行修改 RedisTemplate 中的序列化方案,修改 RedisServiceImpl 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class RedisServiceImpl implements RedisService {

@Autowired
private RedisTemplate redisTemplate;

@Override
public void set(String key, Object value, long seconds) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
}

@Override
public Object get(String key) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForValue().get(key);
}

@Override
public boolean del(String key) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate.delete(key);
}
}

当然也可以直接使用 StringRedisTemplate,修改 RedisServiceImpl 如下::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class RedisServiceImpl implements RedisService {

@Autowired
StringRedisTemplate stringRedisTemplate;

@Override
public void set(String key, Object value, long seconds) {
stringRedisTemplate.opsForValue().set(key, value, seconds, TimeUnit.SECONDS);
}

@Override
public Object get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}

@Override
public boolean del(String key) {
return stringRedisTemplate.delete(key);
}
}

另外需要注意 ,Spring Boot 的自动化配置,只能配置单机的 Redis ,如果是 Redis 集群,则所有的东西都需要自己手动配置,关于如何操作 Redis 集群,以后再来和大家分享。

实现 Session 共享


在传统的单服务架构中,一般来说,只有一个服务器,那么不存在 Session 共享问题,但是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:

img

在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上之后,被 Nginx 转发到 Tomcat A 上,然后在 Tomcat A 上往 session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有之前的数据。对于这一类问题的解决,思路很简单,就是将各个服务之间需要共享的数据,保存到一个公共的地方(主流方案就是 Redis):

img

当所有 Tomcat 需要往 Session 中写数据时,都往 Redis 中写,当所有 Tomcat 需要读数据时,都从 Redis 中读。这样,不同的服务就可以使用相同的 Session 数据了。

这样的方案,可以由开发者手动实现,即手动往 Redis 中存储数据,手动从 Redis 中读取数据,相当于使用一些 Redis 客户端工具来实现这样的功能,毫无疑问,手动实现工作量还是蛮大的。

一个简化的方案就是使用 Spring Session 来实现这一功能,Spring Session 就是使用 Spring 中的代理过滤器,将所有的 Session 操作拦截下来,自动的将数据 同步到 Redis 中,或者自动的从 Redis 中读取数据。

对于开发者来说,所有关于 Session 同步的操作都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 一样。

基于上面的依赖,还需要在 pom.xml 中额外引入 Spring Session:

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

注意:这里我使用的 Spring Boot 版本是 2.1.4 ,如果使用当前最新版 Spring Boot2.1.5 的话,除了上面这些依赖之外,需要额外添加 Spring Security 依赖(其他操作不受影响,仅仅只是多了一个依赖,当然也多了 Spring Security 的一些默认认证流程)。

配置完成后 ,就可以使用 Spring Session 了,其实就是使用普通的 HttpSession ,其他的 Session 同步到 Redis 等操作,框架已经自动帮你完成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {

@Value("${server.port}")
Integer port;

@GetMapping("/set")
public String set(HttpSession session) {
session.setAttribute("user", "antonio");
return String.valueOf(port);
}

@GetMapping("/get")
public String get(HttpSession session) {
return session.getAttribute("user") + ":" + port;
}
}

考虑到一会 Spring Boot 将以集群的方式启动 ,为了获取每一个请求到底是哪一个 Spring Boot 提供的服务,需要在每次请求时返回当前服务的端口号,因此这里我注入了 server.port 。接下来 ,项目打包:

img

打包之后,启动项目的两个实例:

1
2
java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar sessionshare-0.0.1-SNAPSHOT.jar --server.port=8081

然后先访问 localhost:8080/set8080 这个服务的 Session 中保存一个变量,访问完成后,数据就已经自动同步到 Redis 中了,再调用 localhost:8081/get 接口,就可以获取到 8080 服务的 session 中的数据。

此时关于 session 共享的配置就已经全部完成了,session 共享的效果我们已经看到了,但是每次访问都是我自己手动切换服务实例,因此,接下来还需要引入 Nginx ,实现服务实例自动切换。参阅文章:Nginx 实现反向代理及负载均衡配置

更多干货请移步:https://antoniopeng.com


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !