【每天学点Spring】Spring Session学习,与Hazelcast的简单示例

【参考】

【本文目标】
主要介绍了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

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

原因是因为:

  • 【第一次访问】,后台落到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。

浏览器发送ID=1,收到的ID却=2

  • 依此类推,因为同一个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:

image.png

image.png

访问/session-content,可以看到map中只有一个值,并且key就是上述的session ID:

key: c83ae489-7535-4280-b37d-397aaea377a2, value: org.springframework.session.MapSession@4c2928e2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容