在初步了解Session的时候,我们就知道Session通常是保存在服务器的内存当中的。每一次客户访问都会携带SessionId,然后在服务器的内存中寻找。虽然这种方式简单快捷,但是仍存在比较明显的缺点。服务器的内存是有限的,所以留给Session的空间不多,因此一旦用户的访问量比较多时内存就会捉襟见肘。而且因为session是存在内存当中,并不是持久化存储,就算服务器性能特别好,但是也不免存在服务器的异常停止和重启,这个时候就会导致会话状态的丢失。
可能你会说采用集群部署,多布置几台服务器就可以解决上面存在的问题,如上图所示的网络结构的集群部署。在这种情况下,用户的请求会首先到负载均衡的服务器上,再根据不同负载策略将请求分发到后面的服务器上。此时,就有这样的可能性发生,用户的登录操作是在Server1上完成的,因此Session的信息是缓存在Server1上,但是Server2和Server3是不知情的,如果同一个用户再一次访问若请求被分配到了Server2和Server3上,就找不到Session,就会要求用户重新登录。这就是集群环境中的会话状态不同步的问题。
集群会话方案
针对于集群环境中会话存在Session共享的问题,目前常见的方案主要分为以下三类:
1) Session保持
2) Session复制
3) Session共享
Session保持
所谓的Session保持也叫做Session粘滞,采用这种方式需要与上面的负载均衡策略相结合。这里的粘滞和保持其实指的是,用户第一次请求被负载均衡LB(Load Balance)服务器转发到Server1上,设置为粘滞和保持后,用户后面的请求始终都是只通过Server1这台服务器,变相的把用户和Server1服务器绑定在一起,这就是所谓的保持和粘滞。实现这种的方式也很简单,通常使用的IP哈希的负载均衡策略将来自相同客户端的请求转发到相同的服务器上。
# upstream代表负载均衡的策略
upstream session_keep{
ip_hash;
server xxxx:8080;
server xxxx:8080;
# down代表server暂时不参与负载均衡
server xxxx:8080 down;
}
上面是Nginx环境下实现Session保持的方式,可以看到非常简便,不需要对Session做任何处理,也达到一定程度上的集群会话。但是通过此方式实现,仍然避免不了访问的服务器发生故障时,用户会被转移到另外一个服务器上。
此外采用会话保持的方式仍有一个很明显的错误——通过IP哈希实现将请求转发到服务器上,但是某家公司或者组织使用的都是同一个IP出口,那么这家公司或组织的员工都会被分发到相同服务器上,而另外一个IP只有一个或者几个用户同时使用。这种情况下就很明显的存在一定程度的负载失衡,且如果公司的员工或组织使用的人数比较多,对服务器的压力也是存在一定的挑战性。
Session复制
此方式顾名思义,就是在集群服务器之间同步复制Session数据,通俗的就是说,用户A在Server1上访问后生成的Session信息,会定时的复制粘贴到Server2、Server3这些其他服务器上去,从而确保集群上的各个实例之会话状态的一致性。这种方式的缺点也是毫无疑问的,任意一个服务器的Session数据发生改变,都会拷贝到其他服务器上,若服务器实列很多的情况下这种同步不仅仅会消耗数据带宽,还会占用大量的资源,而且效率低,也有很严重的延迟。
实现Session复制的方式也很简单,需要借助于Tomcat集群配置。即对每个tomcat节点的server.xml进行配置。具体配置信息如下
<!--
className:指定Cluster使用的类
channelSendOptions:Cluster发送消息的方式,可选项:2,4,8,10
2:Channel.SEND_OPTIONS_USE_ACK(确认发送)
4: Channel.SEND_OPTIONS_SYNCHRONIZED_ACK(同步发送)
8: Channel.SEND_OPTIONS_ASYNCHRONOUS(异步发送)
在异步模式下,可以通过加上确认发送来提高可靠性,此时将channelSendOptions设置为10
-->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="8">
<!--
Manager决定如何管理集群中的Session信息。Tomcat包括BackupManager和DeltaManager等Manager。
BackupManager:是将集群下所有Session信息放到一个备份节点。所有节点均可访问备份节点。
每个节点部署的应用可以不一致
DeltaManager:集群下某一节点生成、改动Session,将复制到其他节点。Tomcat默认的集群Manager。
每个节点部署的应用要一致
expiressSessionsOnShutdown : 设置为true时,一个节点关闭,将导致集群下的所有Session失效。
notifyListenersOnReplication : 集群节点之间Session复制、删除操作是否通知Session Listeners
maxInactiveInterval : 集群下Session的有效时间
-->
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true" />
<!--
Channel是Tomcat节点之间进行通讯的工具。Chanel包括四个组件:
MemberShip(可用节点列表)、 Sender(发送器,负责发送消息)
Receiver(接收器,负责接收消息)、Interceptor(拦截器)也分别对应下面的几个标签
-->
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<!--
Membership维护集群的可用节点列表。它可以检查到新增节点,也可以检查到没有心跳节点
这里默认使用的是多播通信即McastService
address:广播的地址
port:广播端口
frequency:发送心跳(向广播地址发送UDP数据包)的时间间隔
dropTime: Membership在dropTime内未收到某一节点心跳,将从节点列表删除该成员
组播(Multicast):一个发送者和多个接收者之间实现一对多的网络连接。一个发送者同时
给多个接收者传输相同的数据,只需复制一份相同的数据包。它提高了数
据传送效率,减少了骨干网络出现拥塞的可能性。相同组播地址、端口的
Tomcat节点,可以组成集群下的子集群
-->
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000" />
<!--
接收器主要分为两种:BioReceiver(阻塞式)/NioReceiver(非阻塞式)
address:接收消息的地址
port:接收消息的端口
autoBind:端口的区间变化,即当port为4000,autoBind为100,
接收器将在4000-4099间取一个端口,进行监听
selectorTimeout:NioReceiver内轮询的超时时间
maxThreads:线程池最大的线程数
-->
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6" />
<!--
发送器,内嵌Transport组件,Transport是真正负责发送消息的。Transport分为两种:
bio.PooledMultiSender(阻塞式)、nio.PooledParallelSender(非阻塞式)
-->
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
</Sender>
<!--
Cluster的拦截器——TcpFailureDetector
当网络、系统比较繁忙时,Membership可能无法及时更新可用节点列表,此时该拦截器
可以拦截到某个节点关闭的信息,并尝试用TCP连接节点,确保节点真正关闭,从而更新列表
-->
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
<!--
Cluster的拦截器——MessageDispatch15Interceptor
当Cluster的channelSendOptions设置为异步发送时,会先将等待发送的消息进行排队,然
后将排队好的信息转给Sender
-->
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor" />
</Channel>
<!--
此对象可以理解为Tomcat的拦截器:
ReplicationValve:在处理请求前后处理日志,过滤不涉及Session变化的请求
JvmRouteBinderValve:Apache的mod_jk发生错误时,保证同一个客户端请求发送到集群同一个节点
-->
<Valve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter="" />
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
<!--同步集群下所有节点的一致性-->
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false" />
<!--监听Cluster组件接收的消息,启用DeltaManager时,会将Cluster接收的信息通过
ClusterSessionListener传递给DeltaManager-->
<ClusterListener
className="org.apache.catalina.ha.session.ClusterSessionListener" />
</Cluster>
Session共享
与前面两种解决方案相比,Session共享的实用性和可靠性就比较高。Session共享是指将session从服务器中抽离出来,集中存储到独立的数据容器中,并由各个服务器之间共享。目前比较主流的方案是将各个服务器之间需要共享的Session数据,保存到一个公共的地方,例如Redis。
上图展示了Session共享下的服务器网络架构图。由于所有服务器实例单点存取Session,因此集群不同步的问题自然也就不存在,而且独立的数据容器容量相较于服务器内存大很多。另外,与服务本身分离、可持久化等特性使得会话状态不会因为服务停止而丢失。但是由于独立的数据容器增加了网络交互,数据容器的读/写性能、稳定性以及网络I/O速度都成为性能的瓶颈。所以推荐在内网环境下,高可用的部署的Redis服务器是最佳的选择。Redis是基于内存的特性让它拥有极高的读/写性能,高可用部署不仅降低了网络I/O损耗,还提高了稳定性。
如果想把Session共享到Redis中,可由开发者自己实现,但是这种情况下需要考虑如何得到最优存取结构、如何准群清理过期会话以及整合WebSocket等问题。所以才有了Spring Session专门解决集群会话问题。
这里就简单介绍一下基于Redis整合Spring和Spring Session。
首先,需要添加Spring Session的依赖和Redis的相关依赖。
<dependencies>
<!--Spring Session 核心依赖-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
<!--Spring Session对接Redis的依赖-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- Spring Boot整合Redis核心依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
之后就可以配置Spring Session,主要是为了Spring Security提供集群支持的会话注册表即SpringSessionBackedSessionRegistry。
// 启用基于Redis的HttpSession
@EnableRedisHttpSession
public class HttpSessionConfig{
// 提供Redis连接
@Bean
public RedisConnectionFactory connectionFactory(){
return new JedisConnectionFactory();
}
@Autowired
private FindByIndexNameSessionRepository sessionRepository;
// SpringSessionBackedSessionRegistry 是session为Spring Security提供的
// 主要用于集群环境下控制会话并发的会话注册表实现
@Bean
public SpringSessionBackedSessionRegistry sessionRegistry(){
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
// HttpSession事件监听,改用session提供的会话注册表
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}
}
最后将新的会话注册表提供给SpringSecurity即可。
其他的方案
除了上面所说的三种解决方案外,还有其他的方式实现,这里我只举其中的一种——使用Token代替Session。Token是指访问资源的令牌凭据,用于检验请求的合法性,使用于前后端分离的项目。该方式也能实现记住当前用户等信息,常用的就是基于Json Web Token(JWT)认证授权机制,而且使用Token可以避免CSRF攻击,且完美的契合移动端的需求。
参考文章:
Spring Security系列教程21--会话管理之实现集群会话tomcat 集群(1)总结tomcat的server.ml配置cluster的方式,以及Tomcat集群session共享失败的解决方法