在微服务架构中,往往由多个微服务共同⽀撑前端请求,如果涉及到⽤户状态就需要考虑分布式
Session
管
理问题,⽐如⽤户登录请求分发在服务器
A
,⽤户购买请求分发到了服务器
B
, 那么服务器就必须可以获取
到⽤户的登录信息,否则就会影响正常交易。因此,在分布式架构或微服务架构下,必须保证⼀个应⽤服务
器上保存
Session
后,其他应⽤服务器可以同步或共享这个
Session
。
⽬前主流的分布式
Session
管理有两种⽅案。
Session 复制
部分
Web
服务器能够⽀持
Session
复制功能,如
Tomcat
。⽤户可以通过修改
Web
服务器的配置⽂件,让
Web
服务器进⾏
Session
复制,保持每⼀个服务器节点的
Session
数据都能达到⼀致。
这种⽅案的实现依赖于
Web
服务器,需要
Web
服务器有
Session
复制功能。当
Web
应⽤中
Session
数量
较多的时候,每个服务器节点都需要有⼀部分内存⽤来存放
Session
,将会占⽤⼤量内存资源。同时⼤量的
Session
对象通过⽹络传输进⾏复制,不但占⽤了⽹络资源,还会因为复制同步出现延迟,导致程序运⾏错
误。
在微服务架构中,往往需要
N
个服务端来共同⽀持服务,不建议采⽤这种⽅案。
Session 集中存储
在单独的服务器或服务器集群上使⽤缓存技术,如
Redis
存储
Session
数据,集中管理所有的
Session
,所
有的
Web
服务器都从这个存储介质中存取对应的
Session
,实现
Session
共享。将
Session
信息从应⽤中
剥离出来后,其实就达到了服务的⽆状态化,这样就⽅便在业务极速发展时⽔平扩充。
在微服务架构下,推荐采⽤此⽅案,接下来详细介绍。
Session 共享
Session
什么是 Session
由于
HTTP
协议是⽆状态的协议,因⽽服务端需要记录⽤户的状态时,就需要⽤某种机制来识具体的⽤户。
Session
是另⼀种记录客户状态的机制,不同的是
Cookie
保存在客户端浏览器中,⽽
Session
保存在服务器
上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是
Session
。
客户端浏览器再次访问时只需要从该
Session
中查找该客户的状态就可以了。
为什么需要
Session
共享
在互联⽹⾏业中⽤户量访问巨⼤,往往需要多个节点共同对外提供某⼀种服务,如下图:
⽤户的请求⾸先会到达前置⽹关,前置⽹关根据路由策略将请求分发到后端的服务器,这就会出现第⼀次的
请求会交给服务器
A
处理,下次的请求可能会是服务
B
处理,如果不做
Session
共享的话,就有可能出现⽤
户在服务
A
登录了,下次请求的时候到达服务
B
⼜要求⽤户重新登录。
前置⽹关我们⼀般使⽤
lvs
、
Nginx
或者
F5
等软硬件,有些软件可以指定策略让⽤户每次请求都分发到同⼀
台服务器中,这也有个弊端,如果当其中⼀台服务
Down
掉之后,就会出现⼀批⽤户交易失效。在实际⼯作
中我们建议使⽤外部的缓存设备来共享
Session
,避免单个节点挂掉⽽影响服务,使⽤外部缓存
Session
后,我们的共享数据都会放到外部缓存容器中,服务本身就会变成⽆状态的服务,可以随意的根据流量的⼤
⼩增加或者减少负载的设备。
Spring
官⽅针对
Session
管理这个问题,提供了专⻔的组件
Spring Session
,使⽤
Spring Session
在项⽬中
集成分布式
Session
⾮常⽅便。
Spring Session
Spring Session
提供了⼀套创建和管理
Servlet HttpSession
的⽅案。
Spring Session
提供了集群
Session
(
Clustered Sessions
)功能,默认采⽤外置的
Redis
来存储
Session
数据,以此来解决
Session
共
享的问题。
Spring Session
为企业级
Java
应⽤的
Session
管理带来了⾰新,使得以下的功能更加容易实现:
- API 和⽤于管理⽤户会话的实现;
- HttpSession,允许以应⽤程序容器(即 Tomcat)中性的⽅式替换 HttpSession;
- 将 Session 所保存的状态卸载到特定的外部 Session 存储中,如 Redis 或 Apache Geode 中,它们能 够以独⽴于应⽤服务器的⽅式提供⾼质量的集群;
- ⽀持每个浏览器上使⽤多个 Session,从⽽能够很容易地构建更加丰富的终端⽤户体验;
- 控制 Session ID 如何在客户端和服务器之间进⾏交换,这样的话就能很容易地编写 Restful API,因为 它可以从 HTTP 头信息中获取 Session ID,⽽不必再依赖于 cookie;GitChat
- 当⽤户使⽤ WebSocket 发送请求的时候,能够保持 HttpSession 处于活跃状态。
需要说明的很重要的⼀点就是,
Spring Session
的核⼼项⽬并不依赖于
Spring
框架,因此,我们甚⾄能够将
其应⽤于不使⽤
Spring
框架的项⽬中。
Spring
为
Spring Session
和
Redis
的集成提供了组件:
spring-session-data-redis
,接下来演示如何使⽤。
快速集成
引⼊依赖包
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
添加配置⽂件
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnico
de=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 配置
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql= true
# Redis 配置
# Redis 数据库索引(默认为0)
spring.redis.database=0
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务器连接端⼝
spring.redis.port=6379
# Redis 服务器连接密码(默认为空)
spring.redis.password=
# 连接池最⼤连接数(使⽤负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.shutdown-timeout=100
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
整体配置分为三块:数据库配置、
JPA
配置、
Redis
配置,具体配置项在前⾯课程都有所介绍。
在项⽬中创建
SessionConfifig
类,使⽤注解配置其过期时间。
GitChat
Session 配置:
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
}
maxInactiveIntervalInSeconds:
设置
Session
失效时间,使⽤
Redis Session
之后,原
Spring Boot
中
的
server.session.timeout
属性不再⽣效。
仅仅需要这两步
Spring Boot
分布式
Session
就配置完成了。
测试验证
我们在
Web
层写两个⽅法进⾏验证。
@RequestMapping(value = "/setSession")
public Map<String, Object> setSession (HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
request.getSession().setAttribute("message", request.getRequestURL());
map.put("request Url", request.getRequestURL());
return map;
}
上述⽅法中获取本次请求的请求地址,并把请求地址放⼊
Key
为
message
的
Session
中,同时结果返回⻚
⾯。
@RequestMapping(value = "/getSession")
public Object getSession (HttpServletRequest request){
Map<String, Object> map = new HashMap<>();
map.put("sessionId", request.getSession().getId());
map.put("message", request.getSession().getAttribute("message"));
return map;
}
getSession()
⽅法获取
Session
中的
Session Id
和
Key
为
message
的信息,将获取到的信息封装到
Map
中
并在⻚⾯展示。
在测试前我们需要将项⽬
spring-boot-redis-session
复制⼀份,改名为
spring-boot-redis-session-1
并将端⼝
改为:
9090(server.port=9090)
。修改完成后依次启动两个项⽬。
⾸先访问
8080
端⼝的服务,浏览器输⼊⽹址
http://localhost:8080/setSession
,返
回:
{"request Url":"http://localhost:8080/setSession"}
;浏览器栏输⼊⽹址
http://localhost:8080/getSession
,返回信息如下:
{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}
说明
Url
地址信息已经存⼊到
Session
中。
访问
9090
端⼝的服务,浏览器栏输⼊⽹址
http://localhost:9090/getSession
,返回信息如下:
{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}
通过对⽐发现,
8080
和
9090
服务返回的
Session
信息完全⼀致,说明已经实现了
Session
共享。
模拟登录
在实际中作中常常使⽤共享
Session
的⽅式去保存⽤户的登录状态,避免⽤户在不同的⻚⾯多次登录。我们
来简单模拟⼀下这个场景,假设有⼀个
index
⻚⾯,必须是登录的⽤户才可以访问,如果⽤户没有登录给出
请登录的提示。在⼀台实例上登录后,再次访问另外⼀台的
index
看它是否需要再次登录,来验证统⼀登录
是否成功。
添加登录⽅法,登录成功后将⽤户信息存放到
Session
中。
@RequestMapping(value = "/login")
public String login (HttpServletRequest request,String userName,String password){
String msg="logon failure!";
User user= userRepository.findByUserName(userName);
if (user!=null && user.getPassword().equals(password)){
request.getSession().setAttribute("user",user);
msg="login successful!";
}
return msg;
}
通过
JPA
的⽅式查询数据库中的⽤户名和密码,通过对⽐判断是否登录成功,成功后将⽤户信息存储到
Session
中。
在添加⼀个登出的⽅法,清除掉⽤户的
Session
信息。
@RequestMapping(value = "/loginout")
public String loginout (HttpServletRequest request){
request.getSession().removeAttribute("user");
return "loginout successful!";
}
定义
index
⽅法,只有⽤户登录之后才会看到:
index content
,否则提示请先登录。
@RequestMapping(value = "/index")
public String index (HttpServletRequest request){
String msg="index content";
Object user= request.getSession().getAttribute("user");
if (user==null){
msg="please login first!";
}
return msg;
}
和上⾯⼀样我们需要将项⽬复制为两个,第⼆个项⽬的端⼝改为
9090
,依次启动两个项⽬。在
test
数据库
中的
user
表添加⼀个⽤户名为
neo
,密码为
123456
的⽤户,脚本如下:
INSERT INTO `user` VALUES ('1', 'ityouknow@126.com', 'smile', '123456', '2018', 'n
eo');
也可以利⽤
Spring Data JPA
特性在应⽤启动时完成数据初始化:当配置
spring.jpa.hibernate.ddl-auto
: create-drop
,在应⽤启动时,⾃动根据
Entity
⽣成表,并且执⾏
classpath
下的
import.sql
。
⾸先测试
8080
端⼝的服务,直接访问⽹址
http://localhost:8080/index
,返回:
please login fifirst
!提示请先
登录。我们将验证⽤户名为
neo
,密码为
123456
的⽤户登录。访问地址
http://localhost:8080/login?
userName=neo&password=123456
模拟⽤户登录,返回:
login successful!
,提示登录成功。我们再次访问
地址
http://localhost:8080/index
,返回
index content
说明已经可以查看受限的资源。
再来测试
9090
端⼝的服务,直接访问⽹址
http://localhost:9090/index
,⻚⾯返回
index content
,并没有提
示请先进⾏登录,这说明
9090
服务已经同步了⽤户的登录状态,达到了统⼀登录的⽬的。
我们在
8080
服务上测试⽤户退出系统,再来验证
9090
的⽤户登录状态是否同步失效。⾸先访问地址
http://localhost:8080/loginout
模拟⽤户在
8080
服务上退出,访问⽹址
http://localhost:8080/index
,返回
please login fifirst
!说明⽤户在
8080
服务上已经退出。再次访问地址
http://localhost:9090/index
,⻚⾯返
回:
please login fifirst
!,说明
9090
服务上的退出状态也进⾏了同步。
注意
,本次实验只是简单模拟统⼀登录,实际⽣产中我们会以
Filter
的⽅式对登录状态进⾏校验,在本
课程的最后⼀节课中也会讲到这⽅⾯的内容。
我们最后来看⼀下,使⽤
Redis
作为
Session
共享之后的示意图:
从上图可以看出,所有的服务都将
Session
的信息存储到
Redis
集群中,⽆论是对
Session
的注销、更新都
会同步到集群中,达到了
Session
共享的⽬的。
总结
在微服务架构下,系统被分割成⼤量的⼩⽽相互关联的微服务,因此需要考虑分布式
Session
管理,⽅便平
台架构升级时⽔平扩充。通过向架构中引⼊⾼性能的缓存服务器,将整个微服务架构下的
Session
进⾏统⼀
管理。
Spring Session
是
Spring
官⽅提供的
Session
管理组件,集成到
Spring Boot
项⽬中轻松解决分布式
Session
管理的问题。