[Spring] spring-session + JedisPool 實現 session 共享


 

1、至少導入四個jar包:

jedis

spring-session

spring-data-redis
commons
-pool2

 

2、bean配置

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!-- 對象池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">  
        <property name="maxTotal" value="${redis.pool.maxTotal}"/> <!-- 控制一個pool可分配多少個jedis實例 -->  
        <property name="maxIdle" value="${redis.pool.maxIdle}" />   <!-- 控制一個pool最多有多少個狀態為idle(空閑)的jedis實例 -->  
        <property name="minIdle" value="${redis.pool.minIdle}"/>
        <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />  <!-- 表示當borrow一個jedis實例時,最大的等待時間,如果超過等待時間,則直接拋出JedisConnectionException -->  
        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" /> <!-- 在borrow一個jedis實例時,是否提前進行validate操作;如果為true,則得到的jedis實例均是可用的 -->  
        <property name="testOnReturn" value="${redis.pool.testOnReturn}"/>
         <property name="testWhileIdle" value="${redis.pool.testWhileIdle}"/>
    </bean>
    
    <!-- 工廠實現 -->
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
        destroy-method="destroy">
        <property name="hostName" value="${redis.ip}" />
        <property name="port" value="${redis.port}" />
        <property name="timeout" value="${redis.timeout}" />
        <property name="database" value="${redis.database}" />
        <property name="usePool" value="${redis.usePool}" />
        <property name="poolConfig" ref="jedisPoolConfig" />
    </bean>
    <!-- 模板類 -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
    </bean>
<!-- 使用spring-session把http session放到redis里 --> <bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="maxInactiveIntervalInSeconds" value="${session.maxInactiveIntervalInSeconds}" /> </bean> </beans>

 

3、web.xml中配置過濾器

   <!-- 過濾器 -->
   <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>
    </filter-mapping>

 

 

拓展:

SpringSession官網教程】http://docs.spring.io/spring-session/docs/current/reference/html5/

利用springsession解決共享Session問題】http://www.sjsjw.com/104/002313MYM000506/

使用Spring Session做分布式會話管理】 http://dorole.com/1422/

分布式架構下的會話追蹤實踐【基於Cookie和Redis實現】】http://doc.okbase.net/Hello_Nick_Xu/archive/92878.html

1、為什么需要spring-session?它主要解決了哪方面的問題?

 在單台Tomcat應用中,通常使用session保存用戶的會話數據。

 面對高並發的場景,一台Tomcat難當大任,通常我們會使用Nginx在前端攔截用戶請求,轉發給后端的Tomcat服務器群組。在集群環境下,怎么才能做到session數據在多台Tomcat之間的共享呢?

 當然我們可以在多台Tomcat之間進行session數據的相互復制。這樣做的代價是巨大的,尤其是后端Tomcat服務器比較多的情況下(幾十台、甚至上百台Tomcat),session數據在Tomcat之間的相互復制,將消耗大量的系統開銷、甚至引發網絡廣播風暴,影響服務器的正常運行。

 這時候可以考慮將session數據進行集中存儲,比較常見的是使用Memcached來存放會話數據。

 但是使用Memcached有着諸多限制,比如:對存放對象大小的限制、無法進行數據的持久化等。一般使用Redis來充當Memcached的角色,同時使用Spring Data Redis來完成對Redis的操作。

 ====================================================================================================

 HttpSession是通過Servlet容器創建和管理的,像Tomcat/Jetty都是保存在內存中的。而如果我們把web服務器搭建成分布式的集群,然后利用LVS或Nginx做負載均衡,那么來自同一用戶的Http請求將有可能被分發到兩個不同的web站點中去。

 那么問題就來了,這樣可能導致在某次訪問時獲取不到session,如何保證不同的web站點能夠共享同一份session數據呢?

 最簡單的想法就是把session數據保存到內存以外的一個統一的地方,例如Memcached/Redis等數據庫中。那么問題又來了,如何替換掉Servlet容器創建和管理HttpSession的實現呢? 

1)設計一個Filter,利用HttpServletRequestWrapper,實現自己的 getSession()方法,接管創建和管理Session數據的工作。spring-session就是通過這樣的思路實現的。 
  這里也有一個【session保存到redis簡單實現】例子,就是仿造這種思路實現的。
  http://blog.csdn.net/ppt0501/article/details/46700221 (
2)利用Servlet容器提供的插件功能,自定義HttpSession的創建和管理策略,並通過配置的方式替換掉默認的策略。不過這種方式有個缺點,就是需要耦合Tomcat/Jetty等Servlet容器的代碼。這方面其實早就有開源項目了,例如memcached-session-manager,以及tomcat-redis-session-manager。暫時都只支持Tomcat6/Tomcat7。

 

2、什么是spring-session?

Spring Session是Spring的項目之一,GitHub地址:https://github.com/spring-projects/spring-session。 
Spring Session提供了一套創建和管理Servlet HttpSession的方案。Spring Session提供了集群Session(Clustered Sessions)功能,默認采用外置的Redis來存儲Session數據,以此來解決Session共享的問題。
下面是來自官網的特性介紹:
Spring Session provides the following features:
API and implementations
for managing a user's session HttpSession - allows replacing the HttpSession in an application container (i.e. Tomcat) neutral way Clustered Sessions - Spring Session makes it trivial to support clustered sessions without being tied to an application container specific solution. Multiple Browser Sessions - Spring Session supports managing multiple users' sessions in a single browser instance (i.e. multiple authenticated accounts similar to Google). RESTful APIs - Spring Session allows providing session ids in headers to work with RESTful APIs WebSocket - provides the ability to keep the HttpSession alive when receiving WebSocket messages

 

3、集成SpringSession的4個正確姿勢

(1)導入相關jar/配置相關依賴

(2)編寫一個配置類,用來啟用RedisHttpSession功能,並向Spring容器中注冊一個RedisConnectionFactory。/或者通過xml配置

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7200)
public class RedisHttpSessionConfig {

    @Bean
    public RedisConnectionFactory connectionFactory() {
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
        connectionFactory.setPort(6379);
        connectionFactory.setHostName("10.18.15.190");
        return connectionFactory;
    }
}

(3)將RedisHttpSessionConfig加入到WebInitializer#getRootConfigClasses()中,讓Spring容器加載RedisHttpSessionConfig類。WebInitializer是一個自定義的AbstractAnnotationConfigDispatcherServletInitializer實現類,該類會在Servlet啟動時加載(當然也可以采用別的加載方法,比如采用掃描@Configuration注解類的方式等等)。 / 或者通過xml配置

//該類采用Java Configuration,來代替web.xml   
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{Config1.class, Config2.class, RedisHttpSessionConfig.class};
    }
    
    //......
}

 

(4)第四步,編寫一個一個AbstractHttpSessionApplicationInitializer實現類,用於向Servlet容器中添加springSessionRepositoryFilter。  / 或者通過xml配置

import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;

public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
}

 

4、SpringSession原理

(1)前面集成spring-sesion的第二步中,編寫了一個配置類RedisHttpSessionConfig,它包含注解@EnableRedisHttpSession,並通過@Bean注解注冊了一個RedisConnectionFactory到Spring容器中。 
而@EnableRedisHttpSession注解通過Import,引入了RedisHttpSessionConfiguration配置類。該配置類通過@Bean注解,向Spring容器中注冊了一個SessionRepositoryFilter。

(SessionRepositoryFilter的依賴關系:SessionRepositoryFilter --> SessionRepository --> RedisTemplate --> RedisConnectionFactory)

package org.springframework.session.data.redis.config.annotation.web.http;

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
    //......
    
    @Bean
    public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
        //......
        return template;
    }
    
    @Bean
    public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
        //......
        return sessionRepository;
    }
    
    @Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
        //......
        return sessionRepositoryFilter;
    }
    
    //......
}

(2)集成spring-sesion的第四步中,我們編寫了一個SpringSessionInitializer 類,它繼承自AbstractHttpSessionApplicationInitializer。該類不需要重載或實現任何方法,它的作用是在Servlet容器初始化時,從Spring容器中獲取一個默認名叫sessionRepositoryFilter的過濾器類(之前沒有注冊的話這里找不到會報錯),並添加到Servlet過濾器鏈中。 

package org.springframework.session.web.context;

/**
 * Registers the {@link DelegatingFilterProxy} to use the
 * springSessionRepositoryFilter before any other registered {@link Filter}. 
 *
 * ......
 */
@Order(100)
public abstract class AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer {

    private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";

    public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";

    //......

    public void onStartup(ServletContext servletContext)
            throws ServletException {
        beforeSessionRepositoryFilter(servletContext);
        if(configurationClasses != null) {
            AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
            rootAppContext.register(configurationClasses);
            servletContext.addListener(new ContextLoaderListener(rootAppContext));
        }
        insertSessionRepositoryFilter(servletContext);//注冊一個SessionRepositoryFilter
        afterSessionRepositoryFilter(servletContext);
    }

    /**
     * Registers the springSessionRepositoryFilter
     * @param servletContext the {@link ServletContext}
     */
    private void insertSessionRepositoryFilter(ServletContext servletContext) {
        String filterName = DEFAULT_FILTER_NAME;//默認名字是springSessionRepositoryFilter
        DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(filterName);//該Filter代理會在初始化時從Spring容器中查找springSessionRepositoryFilter,之后實際會使用SessionRepositoryFilter進行doFilter操作         
        String contextAttribute = getWebApplicationContextAttribute();
        if(contextAttribute != null) {
            springSessionRepositoryFilter.setContextAttribute(contextAttribute);
        }
        registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
    }
    
    //......
}

SessionRepositoryFilter是一個優先級最高的javax.servlet.Filter,它使用了一個SessionRepositoryRequestWrapper類接管了Http Session的創建和管理工作。 
注意下面給出的是簡化過的示例代碼,與spring-session項目的源代碼有所差異。 

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter implements Filter {

        public doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
                HttpServletRequest httpRequest = (HttpServletRequest) request;
                SessionRepositoryRequestWrapper customRequest =
                        new SessionRepositoryRequestWrapper(httpRequest);

                chain.doFilter(customRequest, response, chain);
        }

        // ...
}

public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {

        public SessionRepositoryRequestWrapper(HttpServletRequest original) {
                super(original);
        }

        public HttpSession getSession() {
                return getSession(true);
        }

        public HttpSession getSession(boolean createNew) {
                // create an HttpSession implementation from Spring Session
        }

        // ... other methods delegate to the original HttpServletRequest ...
}

(3)剩下的問題就是,如何在Servlet容器啟動時,加載下面兩個類。幸運的是,這兩個類由於都實現了WebApplicationInitializer接口,會被自動加載

WebInitializer,負責加載配置類。它繼承自AbstractAnnotationConfigDispatcherServletInitializer,實現了WebApplicationInitializer接口
SpringSessionInitializer,負責添加sessionRepositoryFilter的過濾器類。它繼承自AbstractHttpSessionApplicationInitializer,實現了WebApplicationInitializer接口
在Servlet3.0規范中,Servlet容器啟動時會自動掃描javax.servlet.ServletContainerInitializer的實現類,在實現類中我們可以定制需要加載的類。
在spring-web項目中,有一個ServletContainerInitializer實現類SpringServletContainerInitializer,它通過注解@HandlesTypes(WebApplicationInitializer.class),讓Servlet容器在啟動該類時,
會自動尋找所有的WebApplicationInitializer實現類。
package org.springframework.web;

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    /**
     * Delegate the {@code ServletContext} to any {@link WebApplicationInitializer}
     * implementations present on the application classpath.
     *
     * <p>Because this class declares @{@code HandlesTypes(WebApplicationInitializer.class)},
     * Servlet 3.0+ containers will automatically scan the classpath for implementations
     * of Spring's {@code WebApplicationInitializer} interface and provide the set of all
     * such types to the {@code webAppInitializerClasses} parameter of this method.
     *
     * <p>If no {@code WebApplicationInitializer} implementations are found on the
     * classpath, this method is effectively a no-op. An INFO-level log message will be
     * issued notifying the user that the {@code ServletContainerInitializer} has indeed
     * been invoked but that no {@code WebApplicationInitializer} implementations were
     * found.
     *
     * <p>Assuming that one or more {@code WebApplicationInitializer} types are detected,
     * they will be instantiated (and <em>sorted</em> if the @{@link
     * org.springframework.core.annotation.Order @Order} annotation is present or
     * the {@link org.springframework.core.Ordered Ordered} interface has been
     * implemented). Then the {@link WebApplicationInitializer#onStartup(ServletContext)}
     * method will be invoked on each instance, delegating the {@code ServletContext} such
     * that each instance may register and configure servlets such as Spring's
     * {@code DispatcherServlet}, listeners such as Spring's {@code ContextLoaderListener},
     * or any other Servlet API componentry such as filters.
     *
     * @param webAppInitializerClasses all implementations of
     * {@link WebApplicationInitializer} found on the application classpath
     * @param servletContext the servlet context to be initialized
     * @see WebApplicationInitializer#onStartup(ServletContext)
     * @see AnnotationAwareOrderComparator
     */
    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        //......
    }

}

 

5、如何在 redis 中查看 Session 數據?

(1)Http Session數據在Redis中是以Hash結構存儲的。 
(2)可以看到,還有一個key="spring:session:expirations:1431577740000"的數據,是以Set結構保存的。這個值記錄了所有session數據應該被刪除的時間(即最新的一個session數據過期的時間)。 
127.0.0.1:6379> keys *
1) "spring:session:expirations:1431577740000"
2) "spring:session:sessions:e2cef3ae-c8ea-4346-ba6b-9b3b26eee578"
127.0.0.1:6379> type spring:session:sessions:e2cef3ae-c8ea-4346-ba6b-9b3b26eee578 hash
127.0.0.1:6379> type spring:session:expirations:1431577740000 set 127.0.0.1:6379> hkeys spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b 1) "maxInactiveInterval" 2) "creationTime" 3) "lastAccessedTime" 4) "sessionAttr:attr1" 127.0.0.1:6379> hget spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b sessionAttr:attr1 "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x03" 127.0.0.1:6379> hget spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b creationTime "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01MM\x94(\xec"

 


免責聲明!

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



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