Spring-Redis实现分布式环境下主子域名Session共享

背景
之前一直采用通过注解的方式配置Spring环境下的子域名共享,其基本思路是通过将session放入redis中,然后将使用HTTPSESSION更改为使用SpringSession的方式,使得不同节点共享Session。
一,使用XML的配置方式
首先需要配置Redis的连接工厂(在这里采用的redis的哨兵模式):

	<!-- ############################ Redis 连接池 ↓ ############################### -->
	<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
	       <property name="maxTotal" value="${redis.pool.maxTotal}" />
	       <property name="maxIdle" value="${redis.pool.maxIdle}" />
	       <property name="numTestsPerEvictionRun" value="${redis.pool.numTestsPerEvictionRun}" />
	       <property name="timeBetweenEvictionRunsMillis" value="${redis.pool.timeBetweenEvictionRunsMillis}" />
	       <property name="minEvictableIdleTimeMillis" value="${redis.pool.minEvictableIdleTimeMillis}" />
	       <property name="softMinEvictableIdleTimeMillis" value="${redis.pool.softMinEvictableIdleTimeMillis}" />
	       <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" />
	       <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
	</bean>
	<!-- ############################ Redis 连接池 ↑ ############################### -->

	<!-- ############################ Redis 哨兵集群 ↓ ############################### -->
	<bean id="sentinelConfiguration" class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
	    <property name="master">
	        <bean class="org.springframework.data.redis.connection.RedisNode">
	            <!-- 这个值要和Sentinel中指定的master的名称一致。 -->
	            <property name="name" value="${redis.master.name}"></property>
	        </bean>
	    </property>
	    <!-- 这里是指定Sentinel的IP和端口,不是Master和Slave。 -->
	    <property name="sentinels">
	        <set>
	            <bean class="org.springframework.data.redis.connection.RedisNode">
	                <constructor-arg name="host" value="${redis.sentinel.host1}"></constructor-arg>
	                <constructor-arg name="port" value="${redis.sentinel.port1}"></constructor-arg>
	            </bean>
	            <bean class="org.springframework.data.redis.connection.RedisNode">
	                <constructor-arg name="host" value="${redis.sentinel.host2}"></constructor-arg>
	                <constructor-arg name="port" value="${redis.sentinel.port2}"></constructor-arg>
	            </bean>
	        </set>
	    </property>
	</bean>
	<!-- ############################ Redis 哨兵集群 ↑ ############################### -->
	
	<!-- ############################ Redis ConnectionFactory ↓ ############################### -->
	<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
	    <constructor-arg name="sentinelConfig" ref="sentinelConfiguration"></constructor-arg>
	    <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
	</bean>
	<!-- ############################ Redis ConnectionFactory ↑ ############################### -->

如果只是想实现spring-session各节点共享,而不需要实现子域名的跨域访问,则只需注册RedisHttpSessionConfiguration这个Bean即可使用:

<bean id="redisHttpSessionConfiguration"
		  class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
	<property name="maxInactiveIntervalInSeconds" value="6000"/>
	<property name="httpSessionListeners">
		<list>
			<bean class="com.bsmartd.handler.SessionAccessListener"/>
		</list>
	</property>
</bean>

而后,需要在web.xml中配置过滤器,将所有的请求url都应用这种spring-session,至于这方面的原理稍后在介绍完java代码方式的配置后一起进行介绍:

<!-- spring redis session共享 -->
    <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>

至此,通过xml的方式配置Spring-Session共享的方式已经实现,使用时按照正常的request请求放入

request.getSession().setAttribute("springsession", object);

如果我们需要进行主子域名的共享的话,需要配置Cookie策略,并将其应用到RedisHttpSessionConfiguration中,所以我们需要改写下上面注入RedisHttpSessionConfiguration的配置:
1)首先需要注册Cookie解析器,配置主域名的domain,实现主子域名共享一个cookiepath:

<!-- spring redis session共享 -->
  <bean id="defaultCookieSerializer"
		  class="org.springframework.session.web.http.DefaultCookieSerializer">
	<property name="cookieName" value="MySESSION" />
	<property name="domainName" value="mydomain.com" />
	<property name="cookiePath" value="/" />
</bean>

2)需要注册CookieSession策略:

<!-- spring redis session共享 -->
<bean id="cookieHttpSessionStrategy"
		  class="org.springframework.session.web.http.CookieHttpSessionStrategy">
	<property name="cookieSerializer" ref="defaultCookieSerializer" />
</bean>

3)将session解析策略及在redis中的存放位置都配置到RedisHttpSessionConfiguration之中:

<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration"
	  class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
	<property name="maxInactiveIntervalInSeconds" value="6000"/>
	<property name="redisNamespace" value="MySessionNameSpace"></property>
	<property name="httpSessionStrategy" ref="cookieHttpSessionStrategy"/>
	<property name="defaultRedisSerializer">
		<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
	</property>
</bean>

然后可以实现主子域名分布式环境下的session共享问题,例如:http://web1.mydomain.com/http://web2.mydomain.com/ ,即可以实现session共享。
二,使用java代码的配置方式
我们一步步的进行配置多节点的session共享,首先需要配置Spring-Redis-Session的配置类:

@Configuration
public class RedisConfig{
}

然后需要注册Redis的连接工厂,连接池及哨兵集群Bean:

/**
     * Redis连接工厂
     */
    @Bean
    public JedisConnectionFactory jedisConnectionFactory(JedisPoolConfig jedisPoolConfig,RedisSentinelConfiguration redisSentinelConfiguration){
        return new JedisConnectionFactory(redisSentinelConfiguration,jedisPoolConfig);
    }

    /**
     * Redis 连接池
     * @param property
     * @return
     */
    @Bean
    public JedisPoolConfig jedisPoolConfig(RedisProperty property){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(property.getMaxTotal());
        jedisPoolConfig.setMaxIdle(property.getMaxIdle());
        jedisPoolConfig.setNumTestsPerEvictionRun(property.getNumTestsPerEvictionRun());
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(property.getTimeBetweenEvictionRunsMillis());
        jedisPoolConfig.setMinEvictableIdleTimeMillis(property.getMinEvictableIdleTimeMillis());
        jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(property.getSoftMinEvictableIdleTimeMillis());
        jedisPoolConfig.setMaxWaitMillis(property.getMaxWaitMillis());
        jedisPoolConfig.setTestOnBorrow(property.isTestOnBorrow());
        return jedisPoolConfig;
    }

    /**
     * Redis哨兵集群
     */
    @Bean
    public RedisSentinelConfiguration redisSentinelConfiguration(RedisProperty property){
        RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
        // 主节点
        redisSentinelConfiguration.setMaster(()-> property.getMasterName());
        // Sentinel哨兵集群
        Set<RedisNode> redisNodes = new HashSet<>();
        redisNodes.add(new RedisNode(property.getHost1(),property.getPort1()));
        redisNodes.add(new RedisNode(property.getHost2(),property.getPort2()));
        redisNodes.add(new RedisNode(property.getHost3(),property.getPort3()));
        redisNodes.add(new RedisNode(property.getHost4(),property.getPort4()));
        redisSentinelConfiguration.setSentinels(redisNodes);

        return redisSentinelConfiguration;
    }

然后需要在类注解上添加@EnableRedisHttpSession注解:

@Configuration
@EnableRedisHttpSession
public class RedisConfig{
}

然后需要配置代理过滤器,将所有的请求都映射到spring-session的filter之上:

public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
}

该类不需要进行任何实现,只需要继承一下即可。
实现主子域名session共享:
需要在RedisConfig中注入相关的Bean(cookie解析器,httpsession策略等)

   /**
    * 配置Spring-Session共享
    */
   @Bean
   public CookieSerializer defaultCookieSerializer(){
       DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
       defaultCookieSerializer.setCookieName("MyESSION");
       defaultCookieSerializer.setDomainName("mydomain.com");
       defaultCookieSerializer.setCookiePath("/");
       return defaultCookieSerializer;
   }

   @Bean
   public CookieHttpSessionStrategy cookieHttpSessionStrategy(CookieSerializer defaultCookieSerializer){
       CookieHttpSessionStrategy cookieHttpSessionStrategy = new CookieHttpSessionStrategy();
       cookieHttpSessionStrategy.setCookieSerializer(defaultCookieSerializer);
       return cookieHttpSessionStrategy;
   }

   @Bean
   public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
       return new GenericJackson2JsonRedisSerializer();
   }

若是我们需要将session存放于Redis的特定目录之下,则需在使用EnableRedisHttpSession标签的时候指定redis的名称空间:

@EnableRedisHttpSession(redisNamespace = "MySessionNameSpace")

测试效果:

启动两个tomcat,两个项目,通过nginx代理到同domain的两个不同的二级域名之下,通过获取sessionid发现两个session的sessionid是一个,说明两个不同二级域名之下两个不同的应用节点实现了session共享。

在这里插入图片描述
查看redis之下确实存在对应结点的session信息:
在这里插入图片描述

原理分析:

从代理过滤器DelegatingFilterProxy谈起,在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>
     <dispatcher>REQUEST</dispatcher>
     <dispatcher>ERROR</dispatcher>
</filter-mapping>

或者代码方式的extends AbstractHttpSessionApplicationInitializer,都是使用
DelegatingFilterProxy代理过滤器,它会将真正的请求代理到springSessionRepositoryFilter这个过滤器之中,所以对于spring-session的处理都是springSessionRepositoryFilter将请求进行了相应的处理。那么这个springSessionRepositoryFilter在代码方式中是如何进行注入的?这就要看我们使用spring-session的标签EnableRedisHttpSession的源码了:
在这里插入图片描述
原来在我们使用@EnableRedisHttpSession注解时就自动将springSessionRepositoryFilter暴露出来,这样:

public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
}

通过这个类,在项目启动的时候该类会自动启动,去寻找到对应的springSessionRepositoryFilter,从而使用请求的过滤绑定。
在这里插入图片描述
@EnableRedisHttpSession标签上自定义的有redisNameSpace的字段属性,即对应着redis存储的命名空间。通过@Configuration我们可以看出这是一个配置类,所以它可以注册Bean,也可以引用其它的配置类,在这里很关键的一点是我们看到它引用了@Import(RedisHttpSessionConfiguration.class)这个配置类,并且@EnableRedisHttpSession即将spring-redis-session的所有配置信息进行设置,我们可能需要自定义Session配置的相关属性,比如我们在前面设置的主子域名的domain,redis的命名空间,session的默认解析器等等,这就需要我们自己扩展RedisHttpSessionConfiguration的相关配置类。
在这里插入图片描述

继续看RedisHttpSessionConfiguration的源码,
在这里插入图片描述我们看到RedisHttpSessionConfiguration继承了ImportAware的接口,按照spring生命周期的加载规则,Aware接口的首先加载,
在这里插入图片描述
可以看到首先便执行了RedisHttpSessionConfiguration的setImportMetadata的方法:
在这里插入图片描述
该方法即将@EnableRedisHttpSession(redisNamespace = “MySessionNameSpace”)标签上设置的redisnamespace,过期使劲等信息赋值到RedisHttpSessionConfiguration对象之中;
而后我们来分析一下它注入的
通过这段代码我们得知配置了redis的模板解析,如果我们传入解析器则使用。
我们看到RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration类,而该类实现了ApplicationContextAware接口,即会将spring的上下文注入到此配置类中。
在这里插入图片描述
这是一个配置类,所以它可以注册Bean,在源码中我们看到:
在这里插入图片描述
这是很关键的一个注册Bean,即我们前面说的@EnableRedisHttpSession标签会暴露出springSessionRepositoryFilter的过滤器,即是该处的实现。我们看到它设置了相关的session解析策略,其实this指向的,在该配置类的下部我们看到了通过@autowired的方式注入了相关的bean:
在这里插入图片描述
这就对应了我们之前在RedisConfig配置的bean:

 @Bean
   public CookieSerializer defaultCookieSerializer(){
       DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
       defaultCookieSerializer.setCookieName("MyESSION");
       defaultCookieSerializer.setDomainName("mydomain.com");
       defaultCookieSerializer.setCookiePath("/");
       return defaultCookieSerializer;
   }

   @Bean
   public CookieHttpSessionStrategy cookieHttpSessionStrategy(CookieSerializer defaultCookieSerializer){
       CookieHttpSessionStrategy cookieHttpSessionStrategy = new CookieHttpSessionStrategy();
       cookieHttpSessionStrategy.setCookieSerializer(defaultCookieSerializer);
       return cookieHttpSessionStrategy;
   }

你又会好奇,我们不止配置了session策略,我们还配置了session的默认解析器,而在SpringHttpSessionConfiguration中不存在这个注入,

   @Bean
   public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
       return new GenericJackson2JsonRedisSerializer();
   }

以为这属于redisconfiguration的配置项,所以该项bean的注入发生在RedisHttpSessionConfiguration配置类的引用中:
在这里插入图片描述
通过@Qualifier的方式注入,我们得知上面的bean必须命名为springSessionDefaultRedisSerializer(spring的默认bean命名规则)。
当所有的配置项都配置完成,并且注入进来后,即开启了SpringHttpSessionConfiguration的init操作,并结合RedisHttpSessionConfiguration通过@Bean的方式将RedisMessageListenerContainer、RedisTemplate、RedisOperationsSessionRepository 等注入到Spring容器中。
我们再回头看一下springSessionRepositoryFilter这个过滤器,只所以再次分析它,是因为它非常关键,可以说是spring-session实现特别关键的一步:

springSessionRepositoryFilter的作用:

通过返回值我们看到其返回类型为SessionRepositoryFilter,SessionRepositoryFilter的作用就是替换容器默认的javax.servlet.http.HttpSession支持为org.springframework.session.Session。SessionRepositoryFilter含有很多内部类:SessionRepositoryResponseWrapper、SessionRepositoryRequestWrapper、HttpSessionWrapper。
SessionRepositoryResponseWrapper与SessionRepositoryRequestWrapper通过对request与response请求接口的重新编写(HttpServletRequest中的getSession),
在这里插入图片描述
重写了HttpServletRequestWrapper中的getRequestedSessionId,getSession等方法,实现了之前使用HttpSession改用成使用Spring Session的方式:
在这里插入图片描述
所以,springSessionRepositoryFilter的作用实质是将原有的request请求和response都被重新进行了包装,将HTTPSession替换为Spring Session。

终于写完了,自己也是刚开始看源码,所以很多地方都是很粗浅,本文后半部分写原理的逻辑有点混乱,需要进一步的厘清相关的思路。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值