前言
我们都知道,在传统的单机环境中,设置一个用户的会话信息(session),一般用request.getSession()获取session对象,然后就可以setAttribute(key, value)和getAttribute(key)方法操作会话了,而会话时间在servlet.xml中配置。这种基于内存的会话管理方式在简单的单机环境中使用比较普遍。
那么,在分布式环境中,又有哪些处理会话的方式呢?
分布式环境中常用的会话管理方式
一、Session Replication
即session复制,当请求落在某个机器上,将会话存在本地并复制到其他的集群节点上。
这种方式我们需要考虑复制传递会话的机制,如果是异步,那么会发生延迟;如果同步,那么又会收到网络状况的影响,性能和可用性难以保障。
一般的实现方式为配置tomcat实现session复制。
二、Session Sticky
即粘性session,根据一定的路由策略将用户请求和对应的服务器关联,使得每次请求发生的时候都能落在正确的服务器上。
这种方式避免了会话复制,同时带来了一定程度的不可用性,容易造成单点故障。
一般的实现方式是使用负载均衡器(如nginx等)进行路由、编码自定义路由等。
三、集中式会话
即将应用集群的所有节点的会话操作都指向同一个会话集群。
这种方式增加了会话的可靠性,但实现起来略复杂。
常见的实现方式有数据库、memcached、redis等存储介质来存储会话信息。
四、Cookie based
那么,本文主要说明较为流行的第三种集中式会话管理的实现方式。
在开始之前,我们有必要了解一下基于web的会话实现的一般思路:
一般我们通过在用户登录的时候,在浏览器客户端种植cookie,而后每次请求都会携带会话相关的cookie(可以理解为token或者ticket)到服务端,通过后端程序验证会话的有效性以及获取信息。
为了方便,我们通过spring子项目之一spring-session来进行分析,选择当下比较流行的Spring Session With Spring Boot。
Spring Session项目
项目地址:https://docs.spring.io/spring-session/
文档地址:https://docs.spring.io/spring-session/docs/current/reference/html5/
https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html
原理:替代传统的Tomcat的HttpSession,Spring Session将会话信息持久化到Redis。当一个session被创建的时候,Spring Session创建一个包含session id名叫SESSION的Cookie在浏览器客户端。
Spring Session实现了HttpSession接口,优点主要有2点:
1、Clustered Sessions(集中式会话);
2、RESTful APIs(通过设置HTTP Header中cookie名为“SESSION”与客户端进行交互)。
下面我想说的主要是session在Redis中的存储结构:
直接看9.7.4. Storage Details,操作会话的关键命令如下:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000
maxInactiveInterval 1800
lastAccessedTime 1404360000000
sessionAttr:attrName someAttrValue
sessionAttr2:attrName someAttrValue2 EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100 APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800 SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe EXPIRE spring:session:expirations1439245080000 2100
- Redis存储session的数据结构主要是Hash结构;
- session的超时时间并没有直接跟踪在session key本身,而是由一个特殊的session expires key替代,另外注意这里用的是APPEND命令替代SET命令;
- 存储session的过期时间稍微长一些,因为“This means there is no need to check the expiration before using a session.”,也就是说程序执行需要时间,延迟的这5分钟主要是留给程序执行的时间,保证存储session的key的过期时间不违背判断是否过期的key的过期时间;
- 最后两行主要是在一些场景下比如:当使用Spring Session的WebSocket时,删除key或者失效时,WebSocket的连接也需要断掉。由于redis的后台任务的优先级比较低,可能不会触发key的失效,所以每个key的有效期也会追踪到最近的分钟,也就允许我们使用一个后台任务根据配置信息通过“TTL”命令去挨个触发这些可能已失效的key。(注意,这里我用了“可能”是因为我们去删除key的时候可能由于竞争条件导致判断key是否失效出现失误,也因此不用“DEL”命令直接去触发)
我的观点
Spring Session重写了Servlet容器的request、session、filter等,使得开发只需要继续遵循servlet api,为开发提供了极大地便利;并且支持的功能以及实现方式比较丰富(而我们主要关注于Redis的实现)。另外,在会话失效时间的控制上并没有处理的很好,Redis Keyspace Notifications(原文地址:https://redis.io/topics/notifications)策略并不是十分严密:
Basically
expired
events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero.
翻译:一般来说,失效通知事件发生在redis服务端删除key,而不是当失效时间理论上达到0值。
(下篇传送门:构建集中式会话的分析与实践(二))