spring-session實現分布式集群session的共享


前言

  HttpSession是通過Servlet容器創建和管理的,像Tomcat/Jetty都是保存在內存中的。但是我們把應用搭建成分布式的集群,然后利用LVS或Nginx做負載均衡,那么來自同一用戶的Http請求將有可能被分發到多個不同的應用中。那問題來了,如何保證不同的應用能夠共享同一份session數據呢?最簡單的想法,就是把session數據保存到內存以外的一個統一的地方,例如Memcached/Redis等數據庫中。那問題又來了,如何替換掉Servlet容器創建和管理的HttpSession的實現呢?

  1、利用Servlet容器提供的插件功能,自定義HttpSession的創建和管理策略,並通過配置的方式替換掉默認的策略。這方面其實早就有開源項目了,例如memcached-session-manager(可以參考負載均衡+session共享(memcached-session-manager實現),以及tomcat-redis-session-manager。不過這種方式有個缺點,就是需要耦合Tomcat/Jetty等Servlet容器的代碼。

  2、設計一個Filter,利用HttpServletRequestWrapper,實現自己的 getSession()方法,接管創建和管理Session數據的工作。spring-session就是通過這樣的思路實現的。

參考 spring-session之一 初探 spring-session

  本博客不涉及session解釋,關於session大家自行去查資料;關於spring-session的相關概念大家可以去spring官網查閱(http://projects.spring.io/spring-session/)。

單機應用

  我們先來看下單機應用,應用很簡單,就是在session中設置變量,然后獲取這些設置的變量進行展示 ,具體代碼如下

  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.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>
View Code

  web.xml

<?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>
View Code

  SessionServlet.java

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() + "/");
    }
}
View Code

  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>
View Code

  整個項目結構非常簡單,如下如

  

  本地運行起來,效果如下

  火狐瀏覽器與360瀏覽器代表不同的用戶,各自都能獲取各自session中的設置的全部變量,很正常,沒毛病。

分布式集群應用

  單機應用中,session肯定沒問題,就存在本地的servlet容器中,那么在分布式集群中會像單機一樣正常嗎?我們接着往下看

  搭建高可用的、實現負載均衡的分布式集群環境可參考nginx實現請求的負載均衡 + keepalived實現nginx的高可用,沒搭建的需要先把分布式環境搭建起來

  沒設置ession共享

    應用不變,代碼與單機中的完全一致,將代碼部署到分布式集群中去

    全部運行起來,效果如下

    結果是:無論給session設置多少個值,session中的值都獲取不到(離我的預期還是有差距,具體什么差距請看我的問題)

  spring-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>
View Code

    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>
View Code

    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>
View Code

    session-redis.properties

redis.host=192.168.0.221
redis.pass=myredis
redis.port=6379
redis.session.timeout=600

    整個項目結構如下如

    

    將代碼部署到分布式集群中去,重新運行起來,效果如下

 

    效果與單機應用的效果一樣,這也就說明了session共享實現了,我們來看下redis中是否有session數據,如下圖,redis中是存有session信息的

    

  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.221、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>
View Code

    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
View Code

    據我親測,效果與單節點redis的效果是一樣的,我就不放效果圖了,但是大家最好還是去親測一下。

    工程地址:spring-session

注意點

  1、單機應用中,HttpSession是通過Servlet容器創建和管理的,servlet容器一旦停止服務,那么session也隨之消失;但如果session被保存到redis中,只要redis服務沒停且session在有效期間內,那么servlet容器停止服務了,session還是存在的,這有什么好處了,好處就是servlet容器出現閃停閃修復的情況,用戶就不用重新登錄了。

  2、spring中的ContextLoaderListener與DispatcherServlet不知道大家了解不,嚴格的來講這兩者負責加載的bean是有區別的,也最好設置成加載不同的bean,不然可能會發生一些你意想不到的情況。不知道區別的可以去閱讀淺談ContextLoaderListener及其上下文與DispatcherServlet的區別

  3、測試的時候可以從底往高進行測試,也就是說先測試tomcat,再測試nginx,最后測試VIP。

  4、redis中可以手動刪除session,不一定非要等到session過期。

  5、分布式測試的時候,最好在index.jsp加一些標記(例如ip,就寫死成index.jsp所在服務器的ip),用來區分不同的服務器,那樣測試起來更加明顯。

  6、spring-session官網提供的例子中,用注解的方式進行配置的,可我壓根就沒看到web.xml中有spring的配置,但實際上spring容器啟動了,並且實例化了需要的bean,應用也能跑起來,這讓我很是費解,spring容器是什么時候初始化的? 這其實是servlet3.0的新特性,servlet3.0開始支持無web.xml的注解配置方式,而AbstractHttpSessionApplicationInitializer(AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer)就是接入點(就如在web.xml中配置spring一樣),更多的詳細信息需要大家去查閱資料了。

  7、設置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的集群密碼設置。

問題

  1、分布式集群的沒設置session共享的情況中,為什么設置進去的值一個都獲取不到,按我的理解應該是每次返回回來的數據應該是某個tomcat上的session中的數據,當設置的值多了后,每次都應該有值返回,而測試得到的結果卻是無論你設置多少值,沒有任何值返回回來,這里沒搞清楚原因。

  2、jedis這么設置集群密碼,目前還不知道,知道的請留個言; 或者知道lettuce怎么設置redis集群和集群密碼的也可以留個言;再或者有其他方式的也可以留個言; 在此表示感謝了!

參考

  spring-session之一 初探 spring-session

  利用spring session解決共享Session問題

  【Spring】淺談ContextLoaderListener及其上下文與DispatcherServlet的區別

  Spring Session

  探 Spring 3.1之無web.xml式 基於代碼配置的servlet3.0應用

  Redis cluster tutorial

  Redis Cluster

  Redis集群搭建與簡單使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM