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的區別