分布式中session共享的解決方案:spring-session


Session是客戶端與服務器通訊會話跟蹤技術,是服務器與客戶端保持整個通訊的會話基本信息。客戶端在第一次訪問服務器的時候,服務端會響應一個sessionId並且將它存入到本地的Cookie中,在之后的訪問會將Cookie中的sessionId放入到請求頭中去訪問服務器,如果通過這個sessionId沒有找到對應的數據,那么服務器就會創建一個新的sessioinId並且響應給客戶端。分布式Session的一致性說白了就是服務器集群Session共享的問題。

分布式中Session存在的共享問題

假設客戶端第一次訪問服務A,服務A響應返回了一個sessionId並且存入了本地Cookie中。第二次不訪問服務A了,轉去訪問服務B。因為客戶端中的Cookie中已經存有了sessionId,所以訪問服務B的時候,會將sessionId加入到請求頭中,而服務B因為通過sessionId沒有找到相對應的數據,因此它就會創建一個新的sessionId並且響應返回給客戶端。這樣就造成了不能共享Session的問題。

分布式中Session共享問題的解決方案

1.根據Cookie來完成(不安全)。

2.使用Nginx的IP綁定策略,同一個IP只能在指定的同一個機器訪問(不支持負載均衡)。

3.利用數據庫同步Session(效率不高)。

4.使用Tomcat內置的Session同步機制(同步可能會產生延遲)。

5.使用Token代替Session。

6.使用Spring-Session以及集成好的解決方案,存放在Redis中。

項目實例場景還原

啟動兩個Spring Boot項目,端口號分別是8081,8182。

在兩個項目中分別創建SessionSharedController類。

@RestController
public class SessionSharedController {
    @Value("${server.port}")
    private Integer projectPort; // 項目端口

    @RequestMapping("/createSession")
    public String createSession(HttpSession session, String name) {
        session.setAttribute("name", name);
        return "【當前項目端口:" + projectPort + "】 【當前sessionId :" + session.getId() + "】";
    }

    @RequestMapping("/getSession")
    public String getSession(HttpSession session) {
        return "【當前項目端口:" + projectPort + "】 【當前sessionId :" + session.getId()
                + "】 【獲取的姓名:" + session.getAttribute("name") + "】";
    }
}

使用Nginx集群,通過修改nginx.conf配置文件使之支持輪詢策略(默認)的負載均衡。

# 開啟輪詢策略(默認)的負載均衡
upstream balanceserver{
    server 127.0.0.1:8081;
    server 127.0.0.1:8082;
}
# 將請求轉發到負載均衡配置的服務器上
location / {
    proxy_pass  http://balanceserver;
    index  index.html index.htm;
}

我們直接通過輪詢機制來訪問首先向Session中存入一個姓名。

訪問:http://localhost/createSession?name=yanggb
得到:【當前項目端口:8081】 【當前sessionId :D5312CBE049C0F486315CF550BFB255C】

因為我們使用的是默認的輪詢策略,因為這次訪問的是8081端口,那么下次訪問的肯定是8082端口,我們可以直接獲取到剛才存入Session的值。

訪問:http://localhost/getSession
得到:【當前項目端口:8082】 【當前sessionId :D85157E33965BE6D7BB1E1CC0E43208F】 【獲取的姓名:null】

這個時候我們會發現,8082端口中並沒有我們存入的值,並且sessionId也是與8081端口不同。先想一想,這個時候我們是8082端口的服務器,但是之前我們是在8081端口中存入了一個姓名,那么我們現在來看看訪問8081端口是否能獲取到之前存入的姓名yanggb。

訪問:http://localhost/getSession
得到:【當前項目端口:8081】 【當前sessionId :C5E2061BB03CE8FFE3E9FBDA00CFA28C】 【獲取的姓名:null】

顯然,8081端口中也獲取不到之前存入的姓名yanggb。如果仔細地觀察的話,會發現連sessionId都不一樣了。原因是因為,在第二次去訪問負載均衡服務器的時候,訪問的是8082端口的服務器,這個時候客戶端在cookie中獲取到的是第一次訪問8081端口的服務器時響應返回的sessionId,拿這個sessionId去8082端口的服務器上找是找不到的,因此8082端口就重新創建了一個sessionId並將這個sessionI響應返回給客戶端,客戶端拿這個sessionId替換掉了之前的8081端口服務器響應返回的sessionId。這樣,當第三次訪問的是8081端口的服務器的時候,就拿了一個在8081端口的服務器上找不到的sessionId去請求,導致又創建一個新的sessionId。這樣就陷入了反復循環的境地,兩個服務器永遠拿到的是對方生成的sessionId,拿不到自己生成的sessionId。

解決這兩個服務之間Session共享問題的方案:Spring-Session

Spring提供了一個解決方案:Spring-Session用來解決兩個服務之間Session共享的問題。

要使用Spring-Session,需要在pom.xml中添加相關依賴。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring session 與redis應用基本環境配置,需要開啟redis后才可以使用,不然啟動Spring boot會報錯 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

同時,需要修改application.properties配置文件(本地要開啟redis服務)。

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=8
spring.redis.timeout=10000

然后再在代碼中添加Session配置類。

/**
 * 這個類用配置redis服務器的連接
 * maxInactiveIntervalInSeconds為SpringSession的過期時間(單位:秒)
 */
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
    // 冒號后的值為沒有配置文件時,制動裝載的默認值
    @Value("${redis.hostname:localhost}")
    private String hostName;
    @Value("${redis.port:6379}")
    private int port;
    // @Value("${redis.password}")
    // private String password;

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(hostName);
        redisStandaloneConfiguration.setPort(port);
        // redisStandaloneConfiguration.setDatabase(0);
        // redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

初始化Session配置

/**
 * 初始化Session配置
 */
public class RedisSessionInitializer extends AbstractHttpSessionApplicationInitializer {
    public RedisSessionInitializer() {
        super(RedisSessionConfig.class);
    }
}

這個時候再重新跑一次上面的測試,會發現能夠拿到相同的Session信息,也就是實現了Session的共享。

Spring-Sesion實現的原理

當Web服務器接收到請求后,請求會進入對應的Filter進行過濾,將原本需要由Web服務器創建會話的過程轉交給Spring-Session進行創建。Spring-Session會將原本應該保存在Web服務器內存的Session存放到Redis中。然后Web服務器之間通過連接Redis來共享數據,達到Sesson共享的目的。

 

"你離開以后,我遇見過很多女孩,像你的眉,像你的眼,但都不是你。"


免責聲明!

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



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