HttpSession是经过Servlet容器建立和管理的,像Tomcat/Jetty都是保存在内存中的。可是咱们把应用搭建成分布式的集群,而后利用LVS或Nginx作负载均衡,那么来自同一用户的Http请求将有可能被分发到多个不一样的应用中。那问题来了,如何保证不一样的应用可以共享同一份session数据呢?最简单的想法,就是把session数据保存到内存之外的一个统一的地方,例如Memcached/Redis等数据库中。那问题又来了,如何替换掉Servlet容器建立和管理的HttpSession的实现呢?html
一、利用Servlet容器提供的插件功能,自定义HttpSession的建立和管理策略,并经过配置的方式替换掉默认的策略。这方面其实早就有开源项目了,例如memcached-session-manager(能够参考负载均衡+session共享(memcached-session-manager实现),以及tomcat-redis-session-manager。不过这种方式有个缺点,就是须要耦合Tomcat/Jetty等Servlet容器的代码。java
二、设计一个Filter,利用HttpServletRequestWrapper,实现本身的 getSession()方法,接管建立和管理Session数据的工做。spring-session就是经过这样的思路实现的。node
参考 spring-session之一 初探 spring-sessionnginx
本博客不涉及session解释,关于session你们自行去查资料;关于spring-session的相关概念你们能够去spring官网查阅(http://projects.spring.io/spring-session/)。git
咱们先来看下单机应用,应用很简单,就是在session中设置变量,而后获取这些设置的变量进行展现 ,具体代码以下github
pom.xml:web
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yzb.lee</groupId> <artifactId>spring-session</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>spring-session Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>spring-session</finalName> </build> </project>
web.xmlredis
<?xml version="1.0" encoding="UTF-8"?> <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>session</servlet-name> <servlet-class>com.yzb.lee.servlet.SessionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>session</servlet-name> <url-pattern>/session</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
SessionServlet.javaspring
package com.yzb.lee.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SessionServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String attributeName = req.getParameter("attributeName"); String attributeValue = req.getParameter("attributeValue"); req.getSession().setAttribute(attributeName, attributeValue); resp.sendRedirect(req.getContextPath() + "/"); } }
index.jsp数据库
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page isELIgnored="false" %> <!DOCTYPE html> <html lang="en"> <head> <title>Session Attributes</title> </head> <body> <div class="container"> <h1>Description</h1> <p>This application demonstrates how to use a Redis instance to back your session. Notice that there is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested session id is.</p> <h1>Try it</h1> <form class="form-inline" role="form" action="./session" method="post"> <label for="attributeName">Attribute Name</label> <input id="attributeName" type="text" name="attributeName"/> <label for="attributeValue">Attribute Value</label> <input id="attributeValue" type="text" name="attributeValue"/> <input type="submit" value="Set Attribute"/> </form> <hr/> <table class="table table-striped"> <thead> <tr> <th>Attribute Name</th> <th>Attribute Value</th> </tr> </thead> <tbody> <c:forEach items="${sessionScope}" var="attr"> <tr> <td><c:out value="${attr.key}"/></td> <td><c:out value="${attr.value}"/></td> </tr> </c:forEach> </tbody> </table> </div> </body> </html>
整个项目结构很是简单,以下如
本地运行起来,效果以下
火狐浏览器与360浏览器表明不一样的用户,各自都能获取各自session中的设置的所有变量,很正常,没毛病。
单机应用中,session确定没问题,就存在本地的servlet容器中,那么在分布式集群中会像单机同样正常吗?咱们接着往下看
搭建高可用的、实现负载均衡的分布式集群环境可参考nginx实现请求的负载均衡 + keepalived实现nginx的高可用,没搭建的须要先把分布式环境搭建起来
应用不变,代码与单机中的彻底一致,将代码部署到分布式集群中去
所有运行起来,效果以下
结果是:不管给session设置多少个值,session中的值都获取不到(离个人预期仍是有差距,具体什么差距请看个人问题)
应用有所变化,代码与以前有所不一样,具体区别以下(SessionServlet与index.jsp不变)
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yzb.lee</groupId> <artifactId>spring-session</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>spring-session Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>1.3.1.RELEASE</version> <type>pom</type> </dependency> <dependency> <groupId>biz.paluch.redis</groupId> <artifactId>lettuce</artifactId> <version>3.5.0.Final</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.3.4.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>spring-session</finalName> </build> </project>
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app> <display-name>Archetype Created Web Application</display-name> <!-- spring-session config --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath*:spring-session.xml</param-value> </context-param> <!-- 这个filter 要放在第一个 --> <filter> <filter-name>springSessionRepositoryFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSessionRepositoryFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>session</servlet-name> <servlet-class>com.yzb.lee.servlet.SessionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>session</servlet-name> <url-pattern>/session</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>
spring-session.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <!-- 加载properties文件 --> <bean id="configProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:session-redis.properties</value> </list> </property> </bean> <!-- RedisHttpSessionConfiguration --> <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="${redis.session.timeout}" /> <!-- session过时时间,单位是秒 --> </bean> <!--LettuceConnectionFactory --> <bean class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory" p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" /> </beans>
session-redis.properties
redis.host=192.168.0.221 redis.pass=myredis redis.port=6379 redis.session.timeout=600
整个项目结构以下如
将代码部署到分布式集群中去,从新运行起来,效果以下
效果与单机应用的效果同样,这也就说明了session共享实现了,咱们来看下redis中是否有session数据,以下图,redis中是存有session信息的
前面是用的一台redis服务器:192.168.0.221作的session服务器,只有一台的话一旦出现单点故障,那么整个session服务就没了,影响太大。为了不出现单点故障问题,须要搭建一个session集群。搭建集群的时候,登陆认证就不要打开了(requirepass注释不要打开,具体缘由后续会有说明)
redis集群环境
192.168.0.221:3个节点(7000,7001,7002)
192.168.0.223:3个节点(7003,7004,7005)
redis集群搭建的过程具体可参考Redis集群搭建与简单使用
redis各个节点搭建成功以后,启动状况以下
192.168.0.221
192.168.0.223
# ./redis-trib.rb create --replicas 1 192.168.0.221:7000 192.168.0.221:7001 192.168.0.221:7002 192.168.0.223:7003 192.168.0.223:7004 192.168.0.223:7005
随便在哪一台(192.168.0.22一、192.168.0.223中任意一台)执行如上命令便可,若出现下图信息,则表示集群搭建成功
redis集群已经搭建好,接下来就是将redis集群应用到咱们的工程中,代码是在spring-sesson实现session共享的基础上进行的,有差异的文件就只有spring-session.xml和session-redis.properties
spring-session.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:annotation-config /> <!-- 加载properties文件 --> <bean id="configProperties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:session-redis.properties</value> </list> </property> </bean> <!-- RedisHttpSessionConfiguration --> <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="${redis.session.timeout}" /> <!-- session过时时间,单位是秒 --> </bean> <!--JedisConnectionFactory --> <bean class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <constructor-arg> <!--redisCluster配置--> <bean class="org.springframework.data.redis.connection.RedisClusterConfiguration"> <constructor-arg> <list> <value>${redis.master1}</value> <value>${redis.master2}</value> <value>${redis.master3}</value> </list> </constructor-arg> </bean> </constructor-arg> </bean> <!--LettuceConnectionFactory --> <!-- 单节点redis --> <!-- <bean class="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory" p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" /> --> </beans>
session-redis.properties
#redis.host=192.168.0.221 #redis.pass=myredis #redis.port=6379 redis.master1=192.168.0.221:7000 redis.master2=192.168.0.223:7003 redis.master3=192.168.0.223:7004 redis.session.timeout=600
据我亲测,效果与单节点redis的效果是同样的,我就不放效果图了,可是你们最好仍是去亲测一下。
工程地址:spring-session
一、单机应用中,HttpSession是经过Servlet容器建立和管理的,servlet容器一旦中止服务,那么session也随之消失;但若是session被保存到redis中,只要redis服务没停且session在有效期间内,那么servlet容器中止服务了,session仍是存在的,这有什么好处了,好处就是servlet容器出现闪停闪修复的状况,用户就不用从新登陆了。
二、spring中的ContextLoaderListener与DispatcherServlet不知道你们了解不,严格的来说这二者负责加载的bean是有区别的,也最好设置成加载不一样的bean,否则可能会发生一些你意想不到的状况。不知道区别的能够去阅读浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别。
三、测试的时候能够从底往高进行测试,也就是说先测试tomcat,再测试nginx,最后测试VIP。
四、redis中能够手动删除session,不必定非要等到session过时。
五、分布式测试的时候,最好在index.jsp加一些标记(例如ip,就写死成index.jsp所在服务器的ip),用来区分不一样的服务器,那样测试起来更加明显。
六、spring-session官网提供的例子中,用注解的方式进行配置的,可我压根就没看到web.xml中有spring的配置,但实际上spring容器启动了,而且实例化了须要的bean,应用也能跑起来,这让我非常费解,spring容器是何时初始化的? 这实际上是servlet3.0的新特性,servlet3.0开始支持无web.xml的注解配置方式,而AbstractHttpSessionApplicationInitializer(AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer)就是接入点(就如在web.xml中配置spring同样),更多的详细信息须要你们去查阅资料了。
七、设置redis集群的时候,若设置了密码登陆(将redis.conf中requirepass打开并设置了本身的密码),那么执行# ./redis-trib.rb create --replicas 1 192.168.0.221:7000 192.168.0.221:7001 192.168.0.221:7002 192.168.0.223:7003 192.168.0.223:7004 192.168.0.223:7005的时候会提示[ERR] Sorry, can't connect to node 192.168.0.221:7000,那么须要将/usr/lib/ruby/gems/1.8/gems/redis-3.3.0/lib/redis/client.rb中的password改为本身的密码便可,固然了,redis的全部实例的密码要一致,或者说所有的redis.conf中密码设置的值要同样,修改/usr/lib/ruby/gems/1.8/gems/redis-3.3.0/lib/redis/client.rb以下
vim /usr/lib/ruby/gems/1.8/gems/redis-3.3.0/lib/redis/client.rb 将client.rb中的password改为本身设置的redis密码 class Redis class Client DEFAULTS = { :url => lambda { ENV["REDIS_URL"] }, :scheme => "redis", :host => "127.0.0.1", :port => 6379, :path => nil, :timeout => 5.0, :password => "myredis", #改为本身的密码 :db => 0, :driver => nil, :id => nil, :tcp_keepalive => 0, :reconnect_attempts => 1, :inherit_socket => false }
以前说过,利用redis集群来存储session的时候,登陆认证不要打开,由于jedis好像还不支持redis的集群密码设置。
一、分布式集群的没设置session共享的状况中,为何设置进去的值一个都获取不到,按个人理解应该是每次返回回来的数据应该是某个tomcat上的session中的数据,当设置的值多了后,每次都应该有值返回,而测试获得的结果倒是不管你设置多少值,没有任何值返回回来,这里没搞清楚缘由。
二、jedis这么设置集群密码,目前还不知道,知道的请留个言; 或者知道lettuce怎么设置redis集群和集群密码的也能够留个言;再或者有其余方式的也能够留个言; 在此表示感谢了!
spring-session之一 初探 spring-session
【Spring】浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别