【参考】
- http请求中session的创建时期
- Guide to Spring Session
- 【Docker学习】在Docker下安装NGINX,通过upstream反向代理
- baeldung示例:spring-session
- Spring-session-hazelcast官网:https://spring.io/projects/spring-session-hazelcast#support
【本文目标】
主要介绍了Session是如何开始工作的,以及和浏览器的交互。
多个后台Instance会造成session不共享的问题。
以及通过Spring Session+Hazelcast进行session共享。
1. Session简单介绍
可以创建一个session listener来观察session何时被创建:
@Configuration
public class MySessionListener implements HttpSessionListener {
public void sessionCreated(HttpSessionEvent se) {
System.out.println("session created: " + se.getSession().getId());
}
public void sessionDestroyed(HttpSessionEvent se) {
System.out.println("session destroyed: " + se.getSession().getId());
}
}
我们再创建用于测试的API,其中方法request.getSession(boolean create):
-
true表示如有需要可以新建一个session -
false表示如果当前没有session就返回null
@GetMapping("hello")
public String hello(HttpServletRequest request) {
HttpSession session = request.getSession(false);
// 打印当前sessionID, 如果没有则打印:null session
System.out.println(session == null ? "null session" : session.getId());
return "hello";
}
@GetMapping("get-session")
public String getSession(HttpServletRequest request) {
// 如果当前没有session, 则创建一个:
HttpSession session = request.getSession();
return session.getId();
}
测试:
- a. 在浏览器中访问API:
/hello,上述的session listener也不会被触发,控制台打印:null session。 - b. 在浏览器中访问API:
/get-session,控制台打印:session created: A1BAB9C03B469EB1759E74D216089C05,再多次访问该接口,session不会被重复create,返回页面的sessionID都是同一个。 - c. 在b的基础上,再访问a,则
打印同一个sessionID。
我们用开发者模式查看request:
-
【首先是步骤a】访问
/hello,因为当前没有session,所以在request和response中都没有任何session相关的header参数。
image.png -
【其次是步骤b】访问
/get-session,可以看到request中没有session相关的参数,但因为在后台API方法中使用了request.getSession()创建了新的session,所以在response中有了Set-Cookie的header,JSESSIONID的值即为后台生成的session的ID。
image.png

- 【最后是步骤c】再次访问
/hello,浏览器在当前Cookies中有JSESSIONID的时候,会保证在每次的request中都加上header=Cookie,值即为步骤2中的session ID。
image.png

上述的测试也是很符合平常的上网浏览习惯的:
如果我们在某个网站进行文章浏览,在没有登陆的时候,右上角的图标会一直显示未登陆。
此时浏览器和后台java程序中都没有session。如果想要收藏某篇文章,这时候就需要点击登陆。输入用户名密码后,网站就会生成一个session会话,以记住我们的登陆状态。
即:后台生成session--> 通过response的header (Set-Cookie: JSESSIONID=xxx)告诉浏览器需要将这个会话ID记住。如果我们想要收藏另一篇文章时,此时就不需要再次登陆了。
即:点击另一个页面,浏览器发现当前有session --> set request header(Cookie:JSESSIONID=xxx) -->后台容器通过JSESSIONID找到步骤2生成的会话。这里的容器通常情况下是tomcat,当然也可能是别的Web容器如weblogic, jetty。
2. 后台Load balance导致的session问题
就上述的web app,如果我们在本地启动两个节点,端口为5010和5011(即有两个tomcat),并通过nginx配置成load balance(关于NGINX的配置,可以参考文章开头的第三个链接),统一通过以下API访问:
在多访问几次后,我们会发现,程序控制台反复的在create session:
原因是因为:
- 【第一次访问】,后台落到5011节点,发现request中没有Cookie,后台会create一个session,此时的ID记为1。
-->再返回给前台,response中会有Set-Cookie: JESESSIONID=1。
浏览器存入ID=1
-【第二次访问】,浏览器中此时有JESSIONID=1,所以会在request中set header=Cookie: JESESSIONID=1 --> 但此时后台落到5010节点上了,并不是第1次访问中的5011。5010节点的tomcat通过ID=1找session,发现找不到session,所以此时5010节点又会重新create一个,ID记为2。 --> 并会把新的ID通过response的Set-Cookie header返回给浏览器。--> 浏览在收到新的ID后,此时会存新的ID。

- 依此类推,因为同一个API,后台确在两个不同的Tomcat中切换,导致每次后台都能重新创建新的session。
3. 如何解决上述的Session问题?
有两个思路:
-
通过
stick sessions解决:如果同一个session ID,在经过nginx的时候,总是跳转到同一个后台Tomcat,那么就不会有Tomcat通过ID找不到之前它创建的session对象问题了,那么问题也就解决了。市面上有这样的webserver配置。
【可以看到用户1,2的请求总是跳到server1中,而用户3的请求,总是会被分发到server2。】
来源:文章第2个链接 -
通过
共享 Session解决:如果后台两个Tomcat实现数据共享,如session是存放在DB中或是分布式缓存中,那么也就可以解决通过ID找不到session对象的问题。
来源:文章第2个链接
3. Spring Session
Spring Session主要是为了更好的管理分布式系统中的session,即实现了上述第2种解决方案:通过共享Session解决。
支持的持久化实现有:JDBC, MongoDB, Redis或Hazelcast,等等......上述的baeldung示例就是用Redis实现的。
这里我用Hazelcast来集成。
Github上有详细的示例,写的非常完整:https://github.com/hazelcast-guides/spring-session-hazelcast/tree/dependabot/maven/com.hazelcast-hazelcast-5.1.3
3.1 依赖
-
Spring boot: 2.7.0,主要是加了spring-boot-starter-parent以及spring-boot-starter-web。 -
Hazelcast: 5.1.3,主要是hazelcast这一个包,mavenrepository。 - 除了以上,还需要加入Spring Session和Hazelcast集成的包,版本在parent中会管理的,
spring-session-hazelcast会自动引入一些包,如spring-session-core等:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-hazelcast</artifactId>
</dependency>
3.2 创建一个配置类SessionConfiguration.class
参考官网文档:https://docs.hazelcast.com/tutorials/spring-session-hazelcast
需要创建一个配置类用来配置HTTP session在Hazelcast中的存储,主要用到一个annotation:@EnableHazelcastHttpSession,这个annotation还可以额外配置参考FlushMode以及SaveMode:
@Configuration
@EnableHazelcastHttpSession
class SessionConfiguration {
// ...
}
配置类的具体内容可以看官网的tutorials:https://docs.hazelcast.com/tutorials/spring-session-hazelcast#create-a-hazelcast-instance-bean
3.3 创建Controller测试
@RestController
public class SessionController {
@Autowired
private HazelcastInstance hazelcastInstance;
@GetMapping("get-session")
public String getSession(HttpServletRequest request) {
HttpSession session = request.getSession();
System.out.println(session.getId());
return session.getId();
}
@GetMapping("session-content")
public Object get() {
Map map = hazelcastInstance.getMap("spring:session:sessions");
for (Object key : map.keySet()) {
System.out.println("key: " + key + ", value: " + map.get(key));
}
return map.size();
}
}
主要创建了两个API:
-
/get-session,打印当前sessionID,如果当前没有session,则创建一个。 -
/session-content,打印存放在hazelcast中的session map。
3.4 测试
跟#2一样,我们为这个程序启动两个节点:端口5010,5011,并通过NGINX进行访问。
首先访问/get-session,多访问几次,可以看到sessionID都为同一个,说明两个节点共享了session,并没有创建新的session:


访问/session-content,可以看到map中只有一个值,并且key就是上述的session ID:
key: c83ae489-7535-4280-b37d-397aaea377a2, value: org.springframework.session.MapSession@4c2928e2






