美文网首页
Session共享

Session共享

作者: 五香牛肉面 | 来源:发表于2019-06-26 13:57 被阅读0次

    嘿,大家好,今天我要更新的内容是session共享。。。

    开发背景

    由于是比较老的项目(struts2+spring2.5+ibatis),且没有使用redis进行缓存,项目中需要缓存的东西通通都放到了session中,我们想要达到这样一个目的,使用Nginx来负载两个tomcat,这样我们在更新或者升级代码的时候,可以保证另外一个项目还可以继续运行,用户还可以继续访问。那么问题来了,当tomcat-A停止运行的时候,另一个tomcat-B中的session中并没有tomcat-A中的我们缓存的数据,则不能保证服务正常运行。

    所以呢,这里有两个解决方案,方案A:将所有存取session的操作改到存取redis中,但是由于存取session的地方太多了,工作量过大,而且麻烦,所以这种方案就被pass了,然后我们采取方案B:实现session的共享的方式,使用这种方式的好处就是,不改变原来的代码逻辑,而且简单。

    顺着这条路我开始研究session共享,最终找到两种可行的方法:

    一:tomcat-session-redis

    使用tomcat+redis实现session共享,下面是操作步骤以及在配置tomcat-session-redis的时候遇到的一些坑

    1.引入三个jar包:commons-pool2-2.2.jar、jedis-2.6.0.jar和tomcat-redis-session-manager-1.2-tomcat-7.jar放在tomcat的 lib下面。(这里博主的jdk和tomcat的版本都是7,大家有兴趣可以尝试下别的版本,点击链接可以下载不同版本的tomcat-redis-session的jar包)

    2.然后tomcat的context.xml配置里增加如下配置,不要加错了哦,博主蠢蠢的把这面这段配置加到了server.xml中导致报错,好久才发现。。。

    <Valve className="com.radiadesign.tomcat.catalina.RedisSessionHandlerValve" />
    <Manager className="com.radiadesign.tomcat.catalina.RedisSessionHandlerValve"
             host="localhost" 
             port="6379" 
             database="0" 
             maxInactiveInterval="60" 
             sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." 
             sentinelMaster="SentinelMasterName"
             sentinels="sentinel-host-1:port,sentinel-host-2:port,.." />
    

    3.在这里需要注意一个问题,就是大家在配置的时候一定要注意看大家下载的tomcat-redis-session.jar包的版本的RedisSessionHandlerValve的对应路径是否正确,最新版本的RedisSessionHandlerValve的类路径可能会是下面这个,大家根据自己下载的选择配置

    <Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
    <Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
             host="localhost" 
             port="6379" 
             database="0" 
             maxInactiveInterval="60" 
             sessionPersistPolicies="PERSIST_POLICY_1,PERSIST_POLICY_2,.." 
             sentinelMaster="SentinelMasterName"
             sentinels="sentinel-host-1:port,sentinel-host-2:port,.."  />
    

    其中sessionPersistPolicies有三种策略,上文中的是默认策略,还有另外两种保存策略,大家可以根据需要选择配置即可

    SAVE_ON_CHANGE:每次session.setAttribute()或被session.removeAttribute()称为会话都将被保存。注意:此功能无法检测对已存储在特定会话属性中的对象所做的更改。权衡:此选项会稍微降低性能,因为会话的任何更改都会将会话同步保存到Redis。

    ALWAYS_SAVE_AFTER_REQUEST:每次请求后强制保存,无论管理器是否检测到会话更改。如果对已存储在特定会话属性中的对象进行更改,则此选项特别有用。权衡:如果并非所有请求都改变了会话,则此选项实际上会增加竞争条件的可能性。

    配置完成后我们分别启动Nginx(Nginx.conf配置在文章底部),redis,tomcat-A,tomcat-B,此时查看两个tomcat服务的sessionId是否一致,如果一致,则说明我们配置成功了。。。

    遇到的问题

    下面是我在配置时遇到的几个问题,大家如果在配置的时候,有遇到和我相同问题的可以参考一下:

    1.配置要放在context.xml中,而不是server.xml。估计只有我会犯这种错误吧,哈哈哈哈~~~

    2.保证tomcat配置session交由redis。

    3.应用的session管理交由tomcat管理,如果应用针对session做了个性化管理,session 共享会失效,博主就是没有发现项目中的web.xml对session做了过期时间的设置,导致session 共享会失效。

    4.session里不能存入null,因为不能getByte(null),这也是我遇到的一个坑,明明都是配置好了,sessionId也已经相同了,后来查看tomcat-redis-session.jar的源码发现,将数据通过getByte()的方式序列化到了redis中,但是项目中存在set一个null到session中,导致一直报错,在相应位置位置做了非空判断之后,session共享正常。。。

    至此Nginx+tomcat+redis配置session共享测试成功!

    二:spring-session

    注意,spring-session这种方式对spring的版本有一定的要求,由于公司项目使用的spring版本较老,不支持spring-session,所以将spring版本升级到4.3。ps:spring session支持的spring版本最低为spring 4.2以上

    具体操作步骤如下:

    1.确保spring的版本在4.2之上,如为满足条件需先升级spring的版本

    2.引入spring session与redis相关的jar包,分别为:commons-pool2-2.4.2.jar,jedis-2.9.0.jar,spring-data-redis-1.7.3.RELEASE.jar,spring-session-1.2.2.RELEASE.jar,由于博主项目较老,而且还不是maven管理的,所以这里直接引入相关jar包,如果是maven项目的小伙伴在pom中直接引入相关jar包即可

    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>
    

    4.applicationContext.xml文件配置相关redis会话连接管理

    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
          <property name="maxTotal" value="100" />
          <property name="maxIdle" value="10" />
    </bean>
    
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
          <property name="hostName" value="127.0.0.1"/>
          <property name="port" value="6379"/>
          <property name="password" value="" />
          <property name="timeout" value="180000"/>
          <property name="usePool" value="true"/>
          <property name="poolConfig" ref="jedisPoolConfig"/>
    </bean>
    
    <bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
          <property name="maxInactiveIntervalInSeconds" value="3000"/>
    </bean>
    

    遇到的问题

    5.至此其实配置已经结束了,但是其实还没完,这样配置完成之后会存在List、Map、TreeMap等集合不能序列化,不能存入redis的问题,spring-session默认使用的序列化方式是JdkSerializationRedisSerializer,所以我们要修改spring-session默认的序列化方式,使用我们自定义的序列化方式SessionSerializer,另外我们要把需要存取redis的实体类实现序列化接口~~~

    RedisHttpSessionConfig :

    package cn.gathub.session;
    
    import org.springframework.context.ApplicationEventPublisher;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
    import org.springframework.data.redis.core.RedisOperations;
    import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.scheduling.annotation.EnableScheduling;
    import org.springframework.session.data.redis.RedisFlushMode;
    import org.springframework.session.data.redis.RedisOperationsSessionRepository;
    import org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration;
    import org.springframework.session.web.http.CookieSerializer;
    import org.springframework.session.web.http.DefaultCookieSerializer;
    import org.springframework.session.web.http.HeaderHttpSessionStrategy;
    import org.springframework.session.web.http.HttpSessionStrategy;
    
    import com.fasterxml.jackson.databind.ser.std.StringSerializer;
    
    import redis.clients.jedis.JedisPoolConfig;
    
    /**
     * Spring Session分布式会话解决方案
     */
    @Configuration
    @EnableScheduling
    public class RedisHttpSessionConfig extends RedisHttpSessionConfiguration {
    
        @Bean(name = "redisHttpSessionConfiguration")
        public RedisHttpSessionConfiguration redisHttpSessionConfiguration() {
            RedisHttpSessionConfiguration sessionConfiguration = new RedisHttpSessionConfiguration();
            sessionConfiguration.setMaxInactiveIntervalInSeconds(1800);
            return sessionConfiguration;
        }
    
        /**
         * Spring Data Redis 的会话存储仓库配置,可选
         */
        @Bean(name = "sessionRepository")
        public RedisOperationsSessionRepository sessionRepository(
                RedisOperations<Object, Object> sessionRedisTemplate,
                ApplicationEventPublisher applicationEventPublisher) {
            this.setMaxInactiveIntervalInSeconds(Integer.valueOf(1800)); // 单位:秒
            this.setRedisNamespace(getApplicationName());
            this.setRedisFlushMode(RedisFlushMode.ON_SAVE);
            return super.sessionRepository(sessionRedisTemplate, applicationEventPublisher);
        }
    
        /**
         * Spring Data Redis 的默认序列化工具,可选
         */
        @Bean(name = "springSessionDefaultRedisSerializer")
        public RedisSerializer springSessionDefaultRedisSerializer() {
            //RedisSerializer defaultSerializer = new JdkSerializationRedisSerializer();
            RedisSerializer defaultSerializer = new SessionSerializer();
            return defaultSerializer;
        }
       
        private String getApplicationName() {
            return "test";
        }
    }
    

    SessionSerializer :

    package cn.gathub.session;
    
    import java.nio.charset.Charset;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.TreeMap;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.SerializationException;
    
    import com.fasterxml.jackson.databind.JavaType;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    
    
    public class SessionSerializer implements RedisSerializer<Object> {
       
       private Log logger = LogFactory.getLog(SessionSerializer.class);
       private ObjectMapper mapper = new ObjectMapper();
       private Charset charset = Charset.forName("utf-8");
       private final String separator = "=";
       private final String classPrefix = "<";
       private final String classSuffix = ">";
       private final String classSeparator = ",";
    
       private Pattern pattern;
    
       public SessionSerializer() {
          pattern = Pattern.compile("<(.*)>");
       }
    
       /**
         * 获取class,包含集合类的泛型
         * <p>暂只支持最多两个泛型,同时集合内数据必须为同一个实现类,不可将泛型声明成父类</p>
         *
         * @param obj 将要序列化的对象
         * @return 没有泛型,形式为java.lang.String<>
         * <p>一个泛型,形式为java.lang.String<java.lang.String></p>
         * <p>两个个泛型,形式为java.lang.String<java.lang.String,java.lang.String></p>
         */
        private String getBegin(Object obj) {
            StringBuilder builder = new StringBuilder(obj.getClass().toString().substring(6) + classPrefix);
            if (obj instanceof List) {
                List list = ((List) obj);
                if (!list.isEmpty()) {
                    Object temp = list.get(0);
                    builder.append(temp.getClass().toString().substring(6));
                }
            } else if (obj instanceof Map) {
                Map map = ((Map) obj);
                Iterator iterator = map.keySet().iterator();
                if (iterator.hasNext()) {
                    Object key = iterator.next();
                    Object value = map.get(key);
                    builder.append(key.getClass().toString().substring(6)).append(classSeparator).append(value.getClass().toString().substring(6));
                }
            } else if (obj instanceof Set) {
                Set set = ((Set) obj);
                Iterator iterator = set.iterator();
     
                if (iterator.hasNext()) {
                    Object value = iterator.next();
                    builder.append(value.getClass().toString().substring(6));
                }
            } else if (obj instanceof TreeMap) {
               TreeMap treeMap = ((TreeMap) obj);
                Iterator iterator = treeMap.keySet().iterator();
                if (iterator.hasNext()) {
                    Object key = iterator.next();
                    Object value = treeMap.get(key);
                    builder.append(key.getClass().toString().substring(6)).append(classSeparator).append(value.getClass().toString().substring(6));
                }
            } 
            builder.append(classSuffix);
            return builder.toString();
        }
     
        @Override
        public byte[] serialize(Object o) throws SerializationException {
            if (o == null)
                return new byte[0];
            try {
                String builder = getBegin(o) +
                        separator +
                        mapper.writeValueAsString(o);
                return builder.getBytes(charset);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
     
        @Override
        public Object deserialize(byte[] bytes) throws SerializationException {
            if (bytes == null || bytes.length == 0) return null;//已被删除的session
            try {
                String temp = new String(bytes, charset);
     
                String cl[] = getClass(temp);
     
                if (cl == null) {
                    throw new RuntimeException("错误的序列化结果=" + temp);
                }
                if (cl.length == 1) {
                    return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), Class.forName(cl[0]));
                } else if (cl.length == 2) {
                    TypeFactory factory = mapper.getTypeFactory();
                    JavaType type = factory.constructParametricType(Class.forName(cl[0]), Class.forName(cl[1]));
                    return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), type);
                } else if (cl.length == 3) {
                    TypeFactory factory = mapper.getTypeFactory();
                    JavaType type = factory.constructParametricType(Class.forName(cl[0]), Class.forName(cl[1]), Class.forName(cl[2]));
                    return mapper.readValue(temp.substring(temp.indexOf(separator) + 1), type);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
     
        /**
         * 解析字符串,获取class
         * <p>一个类型,java.lang.String<>={}</p>
         * <p>两个类型,后面为泛型,java.lang.String<java.lang.String>={}</p>
         * <p>三个类型,后面为泛型,java.lang.String<java.lang.String,java.lang.String>={}</p>
         *
         * @param value 包含class的字符串
         * @return 返回所有类的数组
         */
        private String[] getClass(String value) {
            int index = value.indexOf(classPrefix);
            if (index != -1) {
                Matcher matcher = pattern.matcher(value.subSequence(index, value.indexOf(classSuffix) + 1));
                if (matcher.find()) {
                    String temp = matcher.group(1);
                    if (temp.isEmpty()) {//没有泛型
                        return new String[]{value.substring(0, index)};
                    } else if (temp.contains(classSeparator)) {//两个泛型
                        int nextIndex = temp.indexOf(classSeparator);
                        return new String[]{
                                value.substring(0, index),
                                temp.substring(0, nextIndex),
                                temp.substring(nextIndex + 1)
                        };
                    } else {//一个泛型
                        return new String[]{
                                value.substring(0, index),
                                temp
                        };
                    }
                }
            }
            return null;
        }
    }
    

    配置完成后我们分别启动Nginx(Nginx.conf配置在文章底部),redis,tomcat-A,tomcat-B,此时查看两个tomcat服务的sessionId是否一致,如果一致,则说明我们配置成功了。。。

    以上就是实现session共享的的两种方式。。。

    Nginx.conf配置文件

    使用Nginx负载均配两个tomcat配置如下:

    worker_processes  1;
    
    events {
        worker_connections  1024;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
    
        sendfile        on;
    
        keepalive_timeout  65;
    
        #负载均衡
        #1、轮询(默认)
        #每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
        upstream test{
            server 127.0.0.1:8080;
            server 127.0.0.1:8081;
        }
        #设定虚拟主机配置
        server {
            #侦听80端口
            listen       80;
            #定义使用服务名 一般和域名相同 多个域名用空格分隔
            server_name  127.0.0.1;
    
            #编码
            charset UTF-8;
    
            #URL映射
            location /test {
                #反向代理
                proxy_pass http://test;
                proxy_redirect off;
                proxy_set_header Host $host;
                proxy_set_header  X-Real-IP  $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                client_max_body_size 50m;
                client_body_buffer_size 256k;
                proxy_connect_timeout 30;
                proxy_send_timeout 30;
                proxy_read_timeout 60;
                proxy_buffer_size 16k;
                proxy_buffers 4 32k;
                proxy_busy_buffers_size 64k;
                proxy_temp_file_write_size 64k;
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:Session共享

          本文链接:https://www.haomeiwen.com/subject/whlccctx.html