集群/分布式环境下session处理策略

会话控制

[1]Cookie工作机制

image

浏览器访问服务器时会自动携带Cookie(如果有的话)。

[2]Session工作机制

image

根据浏览器端存储的名为JSESSIONID的Cookie查找服务器端保存的Session对象。

集群是个物理形态,分布式是个工作方式。

  • 分布式:一个业务分拆多个子业务,部署在不同的服务器上

  • 集群:同一个业务,部署在多个服务器上

为什么要处理session?

1
这个问题想必大多数朋友都知道,在搭建完集群或者分布式环境之后,如果不做任何处理的话,网站将频繁的出现用户未登录 的现象。比如:集群中有A、B两台服务器,用户第一次访问网站时,Nginx将用户请求分发到A服务器,这时A服务器给用户创 建了一个Session,当用户第二次访问网站时,假设Nginx将用户请求分发到了B服务器上,而这时B服务器并不存在用户的 Session,所以就会出现用户未登录的情况,这对用户来说是不可忍受的。 所以我们在搭建集群/分布式环境之后,必须考虑的一个问题就是用户访问产生的session如何处理,即session的共享机制 

解决方案

我们将处理Session的方式大致分为三种:

1
2
3
4
5
Session保持(也有人叫黏性Session)

Session复制。

Session共享。

Session保持(或者叫黏性Session、反向代理hash一致性)

image

  • 问题1:具体一个浏览器,专门访问某一个具体服务器,如果服务器宕机,会丢失数据。存在单点故障风险。
  • 问题2:仅仅适用于集群范围内,超出集群范围,负载均衡服务器无效。
1
2
3
4
5
6
Session保持(会话保持)就是将用户锁定到某一个服务器上。比如上面说的例子,用户第一次请求时,负载均衡器 (Nginx)将用户的请求分发到了A服务器上,如果负载均衡器(Nginx)设置了Session保持的话,那么用户以后的 每次请求都会分发到A服务器上,相当于把用户和A服务器粘到了一块,这就是Session保持的原理。Session保持方 案在所有的负载均衡器都有对应的实现。而且这是在负载均衡这一层就可以解决Session问题。  
优点:非常简单,不需要对session做任何处理。

缺点:1、负责不均衡了:由于使用了Session保持,很显然就无法 保证负载的均衡。 2、缺乏容错性:如果后端某台服务器宕机,那么这台服务器的Session丢失,被分配到这台服务请求 的用户还是需要重新登录,所以没有彻底的解决问题。

实现方式:以Nginx为例,在upstream模块配置ip_hash属性即可实现粘性Session
1
容错性,是指软件检测应用程序所运行的软件或硬件中发生的错误并从错误中恢复的能力,通常可以从系统的可靠性、可用 性、可测性等几个方面来衡量。 

Session复制

1
2
3
4
针对Session保持的容错性缺点,我们可以在所有服务器上都保存一份用户的Session信息。这种将每个服务器中的 Session信息复制到其它服务器上的处理办法就称为会话复制。当任何一台服务器上的session发生改变时,该节点会 把session的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要session,以此来保证Session同步。

优点:可容错,各个服务器间的Session能够实时响应。
缺点:将session广播同步给成员,会对网络负荷造成一定 压力

实现方式:tomcat本身已支持该功能

tomcat的会话复制分为两种:

  • 全局复制(DeltaManager):复制会话中的变更信息到集群中的所有其他节点。
  • 非全局复制(BackupManager):它 会把Session复制给一个指定的备份节点。

Session共享(后端统一存储Session数据)

后端存储Session数据时,一般需要使用Redis这样的内存数据库,而一般不采用MySQL这样的关系型数据库。原因如下:

  • Session数据存取比较频繁。内存访问速度快。
  • Session有过期时间,Redis这样的内存数据库能够比较方便实现过期释放。

image

  • 优点

    • 访问速度比较快。虽然需要经过网络访问,但是现在硬件条件已经能够达到网络访问比硬盘访问还要快。

      硬盘访问速度:200M/s

      网络访问速度:1G/s

    • Redis可以配置主从复制集群,不担心单点故障。

③SpringSession使用

以下文档针对在SpringBoot环境下使用

[1]引入依赖

1
2
3
4
5
6
7
8
9
10
<!-- 引入springboot&redis整合场景 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入springboot&springsession整合场景 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

[2]编写配置

1
2
3
4
5
6
# redis配置
spring.redis.host=192.168.56.100
spring.redis.jedis.pool.max-idle=100

# springsession配置
spring.session.store-type=redis

[3]基本原理

概括:SpringSession从底层全方位“**接管**”了Tomcat对Session的管理。

(0)SpringSession需要完成的任务

images

(1)SessionRepositoryFilter

利用Filter原理,在每次请求到达目标方法之前,将原生HttpServletRequest/HttpServletResponse对象包装为SessionRepositoryRequest/ResponseWrapper。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

// 包装对象并不是凭空创建一个相同类型的对象,而是借助原生对象的主体功能,修改有特殊需要的功能。
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);

HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);

try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
wrappedRequest.commitSession();
}
}
(2)HttpSessionStrategy

封装Session的存取策略;cookie还是http headers等方式;

image

(3)SessionRepository

指定存取/删除/过期session操作的repository

image

(4)RedisOperationsSessionRepository

使用Redis将session保存维护起来

image

image

[4]编码实现(因为实在简单,极强的非侵入性,故只写简单写一下)

新建项目,因为要体现Session在服务之间传递,所以至少建立两个模块

Pro23中

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class SessionTestHandler {

@RequestMapping("/test/session/save")
public String testSessionSave(HttpSession session) {

session.setAttribute("attrNameGoodMorning", "attrValueGoodMorning");

return "save completely!";
}

}

Pro24中

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class SessionTestHandler {

@RequestMapping("/test/session/query")
public String testSessionQuery(HttpSession session) {

Object attrValue = session.getAttribute("attrNameGoodMorning");

return "value getted:"+attrValue;
}

}

运行

20200529172035

20200529172110