问题的表象
- 在浏览器A子标签中访问站点首页导航,并点击进入子页面,建立浏览器与服务器的session
- 在同一个浏览器中打开第二个、第三个、第四个子标签访问子页面,并进行相关操作,操作时间必须超过第一个子标签在服务器的session timeout时间
- 打开第五个子标签访问站点首页,这时出现一进入站点就出现session timeout
- 服务端集群有两台机器
分析思路
- 针对问题,监控两台服务器的流量,发现在第一个标签页打开时,浏览器与服务器A建立了连接,并创建了sessionA
- 接下来的第二个、第三个、第四个子标签被打开时,浏览器与服务器B建立了连接,使用的也是sessionA。此时,所有在第2-4个子标签的操作,都是在刷新sessionA在服务器B的session timeout
- 当打开第五个标签页时,浏览器与服务器A建立的连接,还是使用的是sessionA,但是此时sessionA在服务器A上早已经超时,所以当用户第一次打开网站就会报session timeout的错误提示
问题的本质
- 一个浏览器窗口是共享session的,也就是说,打开一个浏览器进程,不管你开多少子标签页,他们都是共享session的。所以当服务器端是个集群,并且没有根据session做负载均衡时,流量会被随机转到不同的服务器上,一旦发生上述场景,就会出现用新标签也打开网站就报session timeout的问题
- 不同的浏览器进程之间是不共享session的,也就是session是跟浏览器进程相关,一个进程获得一个session。这点就是说,当我们新开一个浏览器窗口时,要重新登录,新的浏览器进程需要与服务端重新建立session连接
- 服务器集群之间的服务器是不共享session的
解决思路
- 如果是非生产环境,可是将服务器集群的机器数量减少到1台,但是这个思路治标不治本,并且由于生产环境与非生产环境出现了不一致,很容易让我们忘记在生产环境还要额外的session配置,导致上线失败,因此不推荐(除非生产环境也只用一台机器)
- session共享、session同步,保证服务器集群之间的session一致。在负载均衡器中添加session stickness的机制,保障负载均衡的流量转发是要跟session绑定的,也就是说如果sessionA是服务器A保存的,那么所有sessionA的流量全部要导到服务器A。
但是该方法使得服务器集群失去了动态扩容的优势,极端情况下,当session全部是在一台服务器创建时,就会导致那台服务器响应不过来。因此该方法只有在遗留系统中推荐使用,如果是全新的项目不推荐
- 使用JWT token,消除session,让服务器完全无状态,这样就不会出现session timeout的问题了。推荐使用
关于session的题外话
- session在何时被创建:一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <%@page session="false"%>关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的session对象的来历。由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它
- session何时被删除:
程序调用HttpSession.invalidate() 距离上一次收到客户端发送的session id时间间隔超过了session的超时设置 服务器进程被停止(非持久session)。
- 如何做到在浏览器关闭时删除session:严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力