集群会话方案
在传统的单服务架构中,一般来说,只有一个服务器,那么不存在 Session 共享问题,但是在分布式/集群项目中,Session 共享则是一个必须面对的问题,先看一个简单的架构图:
在这样的架构中,会出现一些单服务中不存在的问题,例如客户端发起一个请求,这个请求到达 Nginx 上之后,被 Nginx 转发到 Tomcat A 上,然后在 Tomcat A 上往 session 中保存了一份数据,下次又来一个请求,这个请求被转发到 Tomcat B 上,此时再去 Session 中获取数据,发现没有之前的数据。
1.1 session 共享
对于这一类问题的解决,目前比较主流的方案就是将各个服务之间需要共享的数据,保存到一个公共的地方(主流方案就是 Redis):
当所有 Tomcat 需要往 Session 中写数据时,都往 Redis 中写,当所有 Tomcat 需要读数据时,都从 Redis 中读。这样,不同的服务就可以使用相同的 Session 数据了。
这样的方案,可以由开发者手动实现,即手动往 Redis 中存储数据,手动从 Redis 中读取数据,相当于使用一些 Redis 客户端工具来实现这样的功能,毫无疑问,手动实现工作量还是蛮大的。
一个简化的方案就是使用 Spring Session 来实现这一功能,Spring Session 就是使用 Spring 中的代理过滤器,将所有的 Session 操作拦截下来,自动的将数据 同步到 Redis 中,或者自动的从 Redis 中读取数据。
对于开发者来说,所有关于 Session 同步的操作都是透明的,开发者使用 Spring Session,一旦配置完成后,具体的用法就像使用一个普通的 Session 一样。
1.2 session 拷贝
session 拷贝就是不利用 redis,直接在各个 Tomcat 之间进行 session 数据拷贝,但是这种方式效率有点低,Tomcat A、B、C 中任意一个的 session 发生了变化,都需要拷贝到其他 Tomcat 上,如果集群中的服务器数量特别多的话,这种方式不仅效率低,还会有很严重的延迟,占用大量带宽。所以这种方案一般作为了解即可。
1.3 粘滞会话
所谓的粘滞会话就是将相同 IP 发送来的请求,通过 Nginx 路由到同一个 Tomcat 上去,这样就不用进行 session 共享与同步了。这是一个办法,但是在一些极端情况下,可能会导致负载失衡(因为大部分情况下,都是很多人用同一个公网 IP)。
所以,Session 共享就成为了这个问题目前主流的解决方案了。
2.Session共享
2.1 创建工程或沿用之前项目工程
首先 创建一个 Spring Boot 工程,引入 Web、Spring Session、Spring Security 以及 Redis:
创建成功之后,pom.xml 文件如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2.2 配置
server.port=8080
spring.security.user.name=summer
spring.security.user.password=123
#配置新增Redis相关
#默认没有密码,你可以自己设置
#spring.redis.password=123
#默认端口号为6379
#单机模式
#spring.redis.port=7001
#spring.redis.host=127.0.0.1
#集群模式
#3主3从服务集群模式
spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006
配置一下 Redis 的基本信息;如果是新建项目,Spring Security 为了简化,你可以将用户名密码直接配置在 application.properties 中了,最后再配置一下项目端口号。如基于原来演示项目,只需增加redis相关配置即可。至于redis集群怎么搭建,可以参考我之前发布的文章 使用docker简单快速搭建redis集群 查看。至此我们的系统中所有的session就已经交过redis进行管理了。
2.3 自定义session测试
配置完成后 ,就可以使用 Spring Session 了,其实就是使用普通的 HttpSession ,其他的 Session 同步到 Redis 等操作,框架已经自动帮你完成了:
@RestController
public class HelloController {
@Value("${server.port}")
Integer port;
@GetMapping("/set")
public String set(HttpSession session) {
session.setAttribute("blog", "http://aliyun.52it.club");
return String.valueOf(port);
}
@GetMapping("/get")
public String get(HttpSession session) {
return String.format("当前服务端口号:%s,blog:%s",port,session.getAttribute("blog"));
}
}
考虑到一会 Spring Boot 将以集群的方式启动 ,为了获取每一个请求到底是哪一个 Spring Boot 提供的服务,需要在每次请求时返回当前服务的端口号,因此这里我注入了 server.port 。
接下来 ,我们分别运行项目,分别端口为8080、8081:
然后先访问 127.0.0.1:8080/set
向 8080
这个服务的 Session
中保存一个变量,第一次访问时会自动跳转到登录页面,输入用户名密码进行登录即可。访问成功后,数据就已经自动同步到 Redis
中 了 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3H8ISwoq-1621404884406)(/Users/summer/Desktop/屏幕快照 2021-05-19 下午12.54.57.png)]
**注意:**Springboot默认的是将Object对象序列化转换为字节数组,所以我们存储的对象需要实现序列化接口。如果需要转化为Json字符串格式存储到缓存中,需要自己编写配置类。此处不过多介绍,自定义redis配置文件我们后期有空介绍。这里简单贴一个配置文件模版仅供参考:
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
//初始化json的序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer);
//设置 value 的序列化方式为 jackson2JsonRedisSerializer
RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
//设置默认超过期时间是100秒
//defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofSeconds(seconds));
//初始化RedisCacheWriter
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
//初始化RedisCacheManager
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
//解决查询缓存转换异常的问题 很重要
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
return cacheManager;
}
}
然后,再调用 127.0.0.1:8081/get
接口,就可以获取到 8080
服务的 session
中的数据:
此时关于 session 共享的配置就已经全部完成了,session 共享的效果我们已经看到了。
3.引入 Nginx
很简单,进入 Nginx 的安装目录的 conf 目录下(默认是在 /usr/local/nginx/conf
),编辑 nginx.conf 文件:
upstream 52it.club
{
server 127.0.0.1:8080 weight=1;
server 127.0.0.1:8081 weight=2;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://52it.club;
proxy_redirect default;
}
}
在这段配置中:
- upstream 表示配置上游服务器
- 52it.club 表示服务器集群的名字,这个可以随意取名字
- upstream 里边配置的是一个个的单独服务
- weight 表示服务的权重,意味者将有多少比例的请求从 Nginx 上转发到该服务上
- location 中的 proxy_pass 表示请求转发的地址,
/
表示拦截到所有的请求,转发转发到刚刚配置好的服务集群中 - proxy_redirect 表示设置当发生重定向请求时,nginx 自动修正响应头数据(默认是 Tomcat 返回重定向,此时重定向的地址是 Tomcat 的地址,我们需要将之修改使之成为 Nginx 的地址)。
配置完成后,将本地的 Spring Boot 打包好的 jar 上传到 Linux ,然后在 Linux 上分别启动两个 Spring Boot 实例:
nohup java -jar security-config-0.0.1-SNAPSHOT.jar --server.port=8080 &
nohup java -jar security-config-0.0.1-SNAPSHOT.jar --server.port=8081 &
其中
- nohup 表示当终端关闭时,Spring Boot 不要停止运行
- & 表示让 Spring Boot 在后台启动
配置完成后,重启 Nginx:
/usr/local/nginx/sbin/nginx -s reload
Nginx 启动成功后,我们首先手动清除 Redis 上的数据,然后访问 http://192.168.0.100/set
表示向 session
中保存数据,这个请求首先会到达 Nginx
上,再由 Nginx
转发给某一个 Spring Boot
实例:
如上,表示端口为 8081
的 Spring Boot
处理了这个 /set
请求,再访问 /get
请求:
可以看到,/get
请求是被端口为 8080 的服务所处理的。
4.总结
本文主要向大家介绍了 Spring Session 的使用,另外也涉及到一些 Nginx 的使用 ,虽然本文较长,但是实际上 Spring Session 的配置没啥,涉及到的配置也都很简单。
如果大家没有在 SSM 架构中用过 Spring Session ,可能不太好理解我们在 Spring Boot 中使用 Spring Session 有多么方便,因为在 SSM 架构中,Spring Session 的使用要配置三个地方 ,一个是 web.xml 配置代理过滤器,然后在 Spring 容器中配置 Redis,最后再配置 Spring Session,步骤还是有些繁琐的,而 Spring Boot 中直接帮我们省去了这些繁琐的步骤!