Spring-session(spring-session-data-redis)实现分布式下Session共享

由于公司项目是单节点的,同事在开发过程中并未做Session共享,由于流量上升后,领导未经过了解直接加机器、加节点;大家可想而知了,项目中运行过程出现很多问题,经过同事们系列排查,才知道是由于上面原因导致拿不到Session。

大家在项目采用Spring-Session时一定要注意项目中Spring的版本,否则会给你带成很多坑,首先,Spring-Session所需的最低SPring版本是3.2.14.RELEASE

一、由于Redis采用Cluster集群方式所以在项目中引入以下版本Jar:


    org.springframework.session
	spring-session-data-redis
	1.3.1.RELEASE


	org.springframework.session
	spring-session
	1.3.1.RELEASE
 

需要注意Spring的版本,下面所引用jar是在Spring 4.3.10.RELEASE所测试,如果低于此版本会报错,项目无法启动

错误如下:

ERROR [RMI TCP Connection(7)-127.0.0.1] - Context initialization failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'redisMessageListenerContainer' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Unsatisfied dependency expressed through constructor argument with index 1 of type [org.springframework.session.data.redis.RedisOperationsSessionRepository]: : Error creating bean with name 'sessionRepository' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.springframework.data.redis.core.RedisOperations]: : Error creating bean with name 'sessionRedisTemplate' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoSuchMethodError: org.springframework.core.serializer.support.DeserializingConverter.(Ljava/lang/ClassLoader;)V; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionRedisTemplate' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoSuchMethodError: org.springframework.core.serializer.support.DeserializingConverter.(Ljava/lang/ClassLoader;)V; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sessionRepository' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [org.springframework.data.redis.core.RedisOperations]: : Error creating bean with name 'sessionRedisTemplate' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoSuchMethodError: org.springframework.core.serializer.support.DeserializingConverter.(Ljava/lang/ClassLoader;)V; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sessionRedisTemplate' defined in class path resource [org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoSuchMethodError: org.springframework.core.serializer.support.DeserializingConverter.(Ljava/lang/ClassLoader;)V
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:752)
 

二、在Web.xml文件加入Spring-Session的入口依赖

 
        springSessionRepositoryFilter
        org.springframework.web.filter.DelegatingFilterProxy
    
    
        springSessionRepositoryFilter
        /*
        REQUEST
        ERROR
    

注意此处filter放置位置一定要放在第一行或是标签后面,否则出现希奇古怪问题。


        contextConfigLocation
        classpath*:config/applicationContext.xml
    

三、本人在项目是采用@Bean注解方式,不是传统的XML注入Bean方式,至于传统XML可以自己去网上百度

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;


/**
 * RedisHttpSessionConfiguration的配置文件
 * 引入RedisHttpSessionConfiguration.class
 * maxInactiveIntervalInSeconds设置session在redis里的超时
 */
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds=3600)
public class RedisHttpSessionConfiguration {
    final static Logger logger = LoggerFactory.getLogger(RedisHttpSessionConfiguration.class);

    //是否为集群模式
    @Value("#{configproperties_disconf['redis.cluster']}")
    private boolean cluster;

    //redis地址(集群下多台使用,号隔开)
    @Value("#{configproperties_disconf['redis.hostAndPort']}")
    private String hostAndPort;

    //密码
    @Value("#{configproperties_disconf['redis.password']}")
    private String password;

    //连接超时时间(单台redis有效)
    @Value("#{configproperties_disconf['redis.connectionTimeout']}")
    private String connectionTimeout;

    //获取连接重试次数(单台redis有效)
    @Value("#{configproperties_disconf['redis.failTimes']}")
    private String failTimes;

    //设置socket调用InputStream读数据的超时时间
    @Value("#{configproperties_disconf['redis.soTimeout']}")
    private String soTimeout;

    //缓存默认过期时间
    @Value("#{configproperties_disconf['redis.expire']}")
    private String expire;

    //单个缓存最大存储字节数
    @Value("#{configproperties_disconf['redis.max.value.size']}")
    private String maxValueSize;

    //
    @Value("#{configproperties_disconf['redis.maxIdle']}")
    private Integer maxIdle;

    //
    @Value("#{configproperties_disconf['redis.maxTotal']}")
    private Integer maxTotal;
    //Cookie域名,
    @Value("#{configproperties_disconf['redis.maxTotal']}")
    private String cookieDomainName;

    private String[] hostAndPorts;

    @Autowired
    private JedisPoolConfig jedisPoolConfig;

    @Autowired
    private JedisConnectionFactory jedisConnectionFactory;

	@Bean
    public static ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }


/*    @Bean
    public CookieHttpSessionStrategy cookieHttpSessionStrategy() {
	    CookieHttpSessionStrategy strategy = new CookieHttpSessionStrategy();
	    strategy.setCookieSerializer(cookieSerializer());
	    return strategy;
	}*/

    @Bean
    public CookieSerializer cookieSerializer() {
        final DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID_CSDN");
        //如果项目是在子域名下使用时,建议直接配置成主域名如下
        serializer.setDomainName("csdn.net");

        serializer.setCookiePath("/");
        //serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        //serializer.setCookieMaxAge(sessionCookieConfig.getMaxAge());
        //serializer.setJvmRoute();
        //serializer.setUseSecureCookie();
        //serializer.setUseBase64Encoding();
        //serializer.setUseHttpOnlyCookie(false);
        //serializer.setRememberMeRequestAttribute(SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
        return serializer;
    }


    /**
     * JedisPoolConfig 配置
     *
     * 配置JedisPoolConfig的各项属性
     *
     * @return
     */

    @Bean
    public JedisPoolConfig jedisPoolConfig(){
        JedisPoolConfig jedisPoolConfig= new JedisPoolConfig();
        //连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
        jedisPoolConfig.setBlockWhenExhausted(true);

        //是否启用pool的jmx管理功能, 默认true
        jedisPoolConfig.setJmxEnabled(true);

        //jedis调用returnObject方法时,是否进行有效检查
        jedisPoolConfig.setTestOnReturn(true);

        //最大空闲连接数, 默认8个
        jedisPoolConfig.setMaxIdle(maxIdle);

        //最大连接数, 默认8个
        jedisPoolConfig.setMaxTotal(maxTotal);

        //获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
        jedisPoolConfig.setMaxWaitMillis(-1);

        //逐出连接的最小空闲时间 默认1800000毫秒(30分钟)
        jedisPoolConfig.setMinEvictableIdleTimeMillis(1800000);

        //最小空闲连接数, 默认0
        jedisPoolConfig.setMinIdle(maxIdle);

        //每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3
        jedisPoolConfig.setNumTestsPerEvictionRun(3);

        //对象空闲多久后逐出, 当空闲时间>该值 且 空闲连接>最大空闲数 时直接逐出,不再根据MinEvictableIdleTimeMillis判断  (默认逐出策略)
        jedisPoolConfig.setSoftMinEvictableIdleTimeMillis(1800000);

        //在获取连接的时候检查有效性, 默认false
        jedisPoolConfig.setTestOnBorrow(false);

        //在空闲时检查有效性, 默认false
        jedisPoolConfig.setTestWhileIdle(false);

        //逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
        jedisPoolConfig.setTimeBetweenEvictionRunsMillis(-1);

        return jedisPoolConfig;
    }

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
    	 try {
             if (StringUtils.isBlank(hostAndPort)) {
                 logger.error("not set redis server Host");
                 return null;
             }

             hostAndPorts = hostAndPort.split(",");
             if (StringUtils.isNotBlank(hostAndPort)) {

             }
             if (cluster) {
                 logger.info("redis sever enable cluster model");
                 RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();

                 for (String hp : hostAndPorts) {
                     String[] args = hp.split(":");
                     logger.error(args[0]+ "=="+args[1]);
                     redisClusterConfiguration.clusterNode(args[0], Integer.valueOf(args[1]).intValue());
                 }

                 JedisConnectionFactory connectionFactory = new JedisConnectionFactory(
                         redisClusterConfiguration, jedisPoolConfig);
                 connectionFactory.setTimeout(3600);
                 return connectionFactory;
             } else {
                 //哨兵模式
                 if ( hostAndPorts!= null && hostAndPorts.length > 1) {
                     logger.info("redis sever enable single sentinel model");
                     RedisSentinelConfiguration redisSentinelConfiguration= new RedisSentinelConfiguration();
                     for (String hp : hostAndPorts) {
                         String[] args = hp.split(":");
                         redisSentinelConfiguration.sentinel(args[0], Integer.valueOf(args[1]));
                     }
                     JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisSentinelConfiguration, jedisPoolConfig);
                     jedisConnectionFactory.setTimeout(3600);
                     return jedisConnectionFactory;
                 } else {//单机模式
                     logger.info("redis sever enable single model");
                     String[] args = hostAndPort.split(":");
                     JedisConnectionFactory jedisConnectionFactory =  new JedisConnectionFactory(jedisPoolConfig);
                     jedisConnectionFactory.setHostName(args[0]);
                     jedisConnectionFactory.setPort(Integer.valueOf(args[1]).intValue());
                     jedisConnectionFactory.setTimeout(3600);
                     return jedisConnectionFactory;
                 }
             }
         } catch (Exception e) {
             logger.error("redis connection error");
         }
         return null;
    }
    

    /**
     * RedisTemplate配置
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);// 如果key是String
        // 需要配置一下StringSerializer,不然key会乱码
        // /XX/XX
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 管理缓存
     *
     * @param redisTemplate
     * @return
     */
    /*@SuppressWarnings("rawtypes")
    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        CustomRedisCacheManager cacheManager = new CustomRedisCacheManager(redisTemplate);
        // rcm.setCacheNames(cacheNames)
        // 设置缓存过期时间
        // rcm.setDefaultExpiration(60);秒
        // 设置value的过期时间
        Map expiresMap = new ConcurrentHashMap();
        // map.put("test", 60L);
        Set cacheNames = new HashSet<>();
        cacheNames.add("device-data");
        cacheNames.add("alarm-event-data");
        cacheNames.add("gps-data");
        cacheNames.add("alarm-event-new-data");
        cacheNames.add("relation-organ-data");
        cacheNames.add("relation-police-data");
        cacheNames.add("platform-config-data");
        cacheNames.add("organ-data");
        cacheNames.add("intercom-platform-data");
        cacheNames.add("alarm-seat-data");
        //expiresMap.put("alarm-seat-data", Long.parseLong(systemConfigRepository.findByKey("expires_alarm-seat-data").getValue()));
        expiresMap.put("alarm-event-new-data", 120L);
        // 设置 by chen
        cacheNames.add("relation-organ-data-new");
        expiresMap.put("relation-organ-data-new", 600L);
        // end
        cacheManager.setCacheNames(cacheNames);
        cacheManager.setExpires(expiresMap);
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }*/
    public static void main(String[] args) {  
        Jedis jedis = new Jedis("25.30.9.4",6379);  
        //ping通显示PONG  
        System.out.println(jedis.ping());//去ping我们redis的主机所在ip和端口  
    }
 
} 
  

下面代码是用于Redis缓存管理使用 

 

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;
 
 
public class CustomRedisCacheManager extends RedisCacheManager {
    private static Logger logger = LoggerFactory.getLogger(CustomRedisCacheManager.class);
 
    public CustomRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }
 
    @Override
    public Cache getCache(String name) {
        return new CustomRedisCacheManager.RedisCacheWrapper(super.getCache(name));
    }
 
    protected static class RedisCacheWrapper implements Cache {
 
        private final Cache delegate;
 
        public RedisCacheWrapper(Cache redisCache) {
            Assert.notNull(redisCache, "delegate cache must not be null");
            this.delegate = redisCache;
        }
 
        @Override
        public String getName() {
            try {
                return delegate.getName();
            } catch (Exception e) {
                return handleException(e);
            }
        }
 
        @Override
        public Object getNativeCache() {
            try {
                return delegate.getNativeCache();
            } catch (Exception e) {
                return handleException(e);
            }
        }
 
        @Override
        public Cache.ValueWrapper get(Object key) {
            try {
                return delegate.get(key);
            } catch (Exception e) {
                return handleException(e);
            }
        }
 
 
 
 
        @Override
        public void put(Object key, Object value) {
            try {
                delegate.put(key, value);
            } catch (Exception e) {
                handleException(e);
            }
        }
 
 
 
        @Override
        public void evict(Object o) {
            try {
                delegate.evict(o);
            } catch (Exception e) {
                handleException(e);
            }
        }
 
        @Override
        public void clear() {
            try {
                delegate.clear();
            } catch (Exception e) {
                handleException(e);
            }
        }
 
        private  T handleException(Exception e) {
            logger.error("redis连接异常", e);
            return null;
        }
    }
}

四、测试代码


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@Controller
@RequestMapping("/session")
public class SessionController { 
    private static final Logger logger = LoggerFactory.getLogger(SessionController.class);
 
    
    @ResponseBody
    @RequestMapping(value = "/getSession", method = RequestMethod.GET)
    public Map  getSession(HttpServletRequest request) {
    	request.getSession().setAttribute("testKey", "[email protected]");
        request.getSession().setMaxInactiveInterval(10*1000);
        String testKey = (String)request.getSession().getAttribute("testKey");
        //SessionManager.setAttribute("kes", "Hello World");
        return ResultUtils.getSuccessResultData(SessionManager.getAttribute("testKey") + "===="+ testKey );
         
    }
 

}

五、可以通过Redis客户端查看是否存入Redis中

Spring-session(spring-session-data-redis)实现分布式下Session共享_第1张图片

使用spring session+redis存储的session如何查看.

​​​​127.0.0.1:6379> keys *
              1) "spring:session:expirations:133337740000"
              2) "spring:session:sessions:eefscef3ae-c8ea-4346-ba6b-9b3b26eee578"
127.0.0.1:6379> type spring:session:sessions:eeefefeae-c8ea-4346-ba6b-9b3b26eee578
                hash
127.0.0.1:6379> hkeys spring:session:sessions:eeefefeae-c8ea-4346-ba6b-9b3b26eee578
              1) "maxInactiveInterval"
              2) "creationTime"
              3) "lastAccessedTime"

存储在redis中的key的简单介绍说明:

//存储 Session 数据,数据类型hash,可以使用type查看
Key:spring:session:sessions:eeefefeae-c8ea-4346-ba6b-9b3b26eee578

//Redis TTL触发Session 过期。(Redis 本身功能),数据类型:String
Key:spring:session:sessions:expires:133337740000

//执行 TTL key ,可以查看剩余生存时间
//定时Job程序触发Session 过期。(spring-session 功能),数据类型:Set
Key:spring:session:expirations:133337740000

六、简单介绍一下Spring-Session在Redis中存储时数据结构形式

查看redis中的值:

127.0.0.1:6379> keys *
1) "spring:session:sessions:expires:fc454e71-c540-4097-8df2-92f88447063f"
2) "spring:session:expirations:1515135000000"
3) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user"
4) "spring:session:sessions:fc454e71-c540-4097-8df2-92f88447063f"

Redis中的存储说明:
​1、spring:session是默认的Redis HttpSession前缀(redis中,我们常用’:’作为分割符)。
2、每一个session都会创建3组数据:

第一组:hash结构,spring-session存储的主要内容

spring:session:sessions:fc454e71-c540-4097-8df2-92f88447063f

hash结构有key和field,如上面的例子:hash的key为"spring:session:sessions"前缀加上fc454e71-c540-4097-8df2-92f88447063f,该key下的field有:

  • field=sessionAttr:qwe,value=123                
  • field=creationTime,value=                         //创建时间
  • field=maxInactiveInterval,value=             //
  • field=lastAccessedTime,value=              //最后访问时间

见上面图

第二组:String结构,用于ttl过期时间记录

spring:session:sessions:expires:fc454e71-c540-4097-8df2-92f88447063f

key为“spring:session:sessions:expires:”前缀+fc454e71-c540-4097-8df2-92f88447063f

value为空

第三组:set结构,过期时间记录

spring:session:expirations:1515135000000

set的key固定为“spring:session:expirations:1515135000000”,set的集合values为:

  • expires:c7fc28d7-5ae2-4077-bff2-5b2df6de11d8  //(一个会话一条)
  • expires:fc454e71-c540-4097-8df2-92f88447063f  //(一个会话一条)

简单提一下:redis清除过期key的行为是一个异步行为且是一个低优先级的行为,用文档中的原话来说便是,可能会导致session不被清除。于是引入了专门的expiresKey,来专门负责session的清除,包括我们自己在使用redis时也需要关注这一点。在开发层面,我们仅仅需要关注第三个key就行了。

你可能感兴趣的:(java)