Session共享


嘿,大家好,今天我要更新的內容是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.jarjedis-2.6.0.jartomcat-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。

 

2.應用的session管理交由tomcat管理,如果應用針對session做了個性化管理,session 共享會失效,博主就是沒有發現項目中的web.xml對session做了過期時間的設置,導致session 共享會失效。

 

3.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;
        }
    }
}

 


今天的更新到這里就結束了,拜拜!!!

感謝一路支持我的人,您的關注是我堅持更新的動力,有問題可以在下面評論留言或隨時與我聯系。。。。。。
QQ:850434439
微信:w850434439
EMAIL:gathub@qq.com


如果有興趣和本博客交換友鏈的話,請按照下面的格式在評論區進行評論,我會盡快添加上你的鏈接。


網站名稱:GatHub-HongHui'S Blog
網站地址:https://www.gathub.cn/
網站描述:不習慣的事越來越多,但我仍在前進…就算步伐很小,我也在一步一步的前進。

網站Logo/頭像:[頭像地址]


免責聲明!

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



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