一、什么是集群及Session共享
集群是一组相互连接并且拥有相同功能的服务器,每个服务器在集群中叫做节点。通过负载均衡服务器的调度,使客户端请求均衡的访问到这些节点中。但是此时会出现一个问题,比如session问题,用户A初次在节点A中进行登录,下一次被负载均衡服务器调度到节点B,而节点B并不没有用户A的session信息,接着又进行重新登录。解决办法也有很多,如使用Redis,但是Tomcat中也提供了一套集群中session同步的方法。
还有这里说的负载均衡服务器,如Nginx,接收用户的请求并通过一定的算法将转发给后台服务器集群中的一台机器。
或许还有其他共享方案,以下只是我使用过的两种。Tomcat自带和Spring-Session+Redis
二、Tomcat集群Session共享
首先不去设置集群配置,编写两个Servlet进行测试,看看会出现的问题。
LoginServlet判断请求参数pass是否为123,是则设置session信息并且重定向到IndexServlet,否则输出认证失败。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet implementation class LoginServlet
*/
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public LoginServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.addHeader("Content-Type","text/plain;charset=utf-8");
if("123".equals( request.getParameter("pass"))) {
request.getSession().setAttribute("name", "张三");
response.sendRedirect("IndexServlet");
}else {
response.getWriter().append("pass 认证失败");
}
}
}
IndexServlet首先获取session信息,如果不存在则调转至LoginServlet,存在则输出name。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet("/IndexServlet")
public class IndexServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public IndexServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.addHeader("Content-Type","text/plain;charset=utf-8");
HttpSession session =request.getSession();
StringBuffer sb =new StringBuffer();
String name =(String) session.getAttribute("name");
if(name==null) {
response.sendRedirect("LoginServlet");
}else {
sb.append("sessionId="+session.getId() +"\n");
sb.append("name="+name);
response.getWriter().append(sb);
}
}
}
准备两个Tomcat,但是要注意修改端口号,主要是以下三个节点,不然端口占用会无法启动。
<Server port="8004" shutdown="SHUTDOWN">
<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Connector port="8008" protocol="AJP/1.3" redirectPort="8443" />
打包后分别放入两个Tomcat的webapps下。
当对8080进行登录后,调转至IndexServlet,并成功显示name。
http://localhost:8080/DynamicWebDemo/LoginServlet?pass=123
但是当对8081进行方式时http://localhost:8081/DynamicWebDemo/IndexServlet,此时会跳转至LoginServlet。
首先Cookie是和端口无关的,也就是8080的Cookie,会被8081的请求所带上,而8081的Tomcat上没有保存8080中的session信息,所以会失败。如果8081重新登录后,由于产生新sessionId,被8080所带上请求,而8080的Tomcat又没有这个session的信息。如次反复,互相顶来顶去。
下面就是解决这个问题,配置Tomcat集群设置。
首先在server.xml中增加<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
,并且在项目的web.xml中增加<distributable />
即可,还有很多设置,这里都采用默认就行。
<distributable />
必须增加。
这时候重启两个Tomcat,首先在8080上进行登录
接着直接访问8081上的IndexServlet,可以发现直接显示了信息,并且sessionId是一样的。
而刚才所说的Cluster配置,还有很多配置,如以下。
可以参考https://tomcat.apache.org/tomcat-9.0-doc/cluster-howto.html或者https://www.mulesoft.com/tcat/tomcat-clustering
<Engine name="Catalina" defaultHost="localhost" jvmRoute="[workername]">
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />
<Channel className="org.apache.catalina.tribes.group.GroupChannel">
<Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4"
port="45564" frequency="500" dropTime="3000" />
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000"
autoBind="100" selectorTimeout="5000" maxThreads="6" />
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
</Sender>
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor" />
</Channel>
<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" />
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" />
</Cluster>
在Tomcat官网也说,对于较小的群集适用,而超过4个节点则不建议使用,节点多了通信开销也大,
三、Spring-Session
Spring-Session 提供了一套创建和管理HttpSession 的方案,是把session信息保存在一个公共的会话仓库中,所有服务器都访问同一个仓库,这样所有服务器的状态就都一致了,他支持存储在Hazelcast 、Redis、MongoDB、关系型数据中,以下主要写如何保存在Redis,来解决 session 共享的问题。
首先要下载安装Redis。Windows安装简单,在linux下,解压后进入目录,执行make(前提安装了编译器)。
之后进入src目录,执行./redis-server就可以启动redis服务,这里就不说redis基本操作了。
之后引入两个jar包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
如果下载慢,改一下mavne的设置,增加阿里云镜像。
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
大多采用默认配置,有密码的配密码,没密码的空着就行。
#redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.session.store-type=redis
启动类上增加EnableRedisHttpSession注解,maxInactiveIntervalInSeconds为session过期时间,单位为秒。
@SpringBootApplication
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
编写测试接口,逻辑和上述一样。
@Controller
@RequestMapping(value = "/")
public class SessionController {
@ResponseBody
@RequestMapping(value = "/login")
public void getSession(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.addHeader("Content-Type","text/plain;charset=utf-8");
if("123".equals( request.getParameter("pass"))) {
request.getSession().setAttribute("name", "张三");
response.sendRedirect("/index");
}else {
response.getWriter().append("pass 认证失败");
}
}
@ResponseBody
@RequestMapping(value = "/index")
public void get(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.addHeader("Content-Type","text/plain;charset=utf-8");
HttpSession session =request.getSession();
StringBuffer sb =new StringBuffer();
String name =(String) session.getAttribute("name");
if(name==null) {
response.sendRedirect("/login");
}else {
sb.append("sessionId="+session.getId() +"\n");
sb.append("name="+name);
response.getWriter().append(sb);
}
}
}
然后打包成jar包。启动两个不同的端口号。
mvn package
java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port=8081
java -jar target/demo-0.0.1-SNAPSHOT.jar --server.port=8080
当对8080登录后,8081上访问/index,同样可以看到信息,10秒后,这个session活过期,同样两个服务都会失效。
还可以对session创建失效进行监听。
@Configuration
public class RedisSessionConfig {
@EventListener
public void onSessionExpired(SessionExpiredEvent expiredEvent) {
String sessionId = expiredEvent.getSessionId();
System.out.println("过期"+sessionId);
}
@EventListener
public void onSessionCreated(SessionCreatedEvent createdEvent) {
String sessionId = createdEvent.getSessionId();
System.out.println("创建"+sessionId);
}
@EventListener
public void onSessionDeleted(SessionDeletedEvent deletedEvent) {
String sessionId = deletedEvent.getSessionId();
System.out.println("删除"+sessionId);
}
}
同时可以通过keys * 查看redis中的key,也可以用redis对hash类型的相关操作命令进行查看一些信息。