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共享的目的。
"你離開以后,我遇見過很多女孩,像你的眉,像你的眼,但都不是你。"