第 4-3 課:使⽤ Redis 實現 Session 共享

在微服務架構中,往往由多個微服務共同⽀撐前端請求,如果涉及到⽤戶狀態就需要考慮分佈式 Session
理問題,⽐如⽤戶登錄請求分發在服務器 A,⽤戶購買請求分發到了服務器 B, 那麼服務器就必須可以獲取
到⽤戶的登錄信息,否則就會影響正常交易。因此,在分佈式架構或微服務架構下,必須保證⼀個應⽤服務
器上保存 Session 後,其他應⽤服務器可以同步或共享這個 Session
⽬前主流的分佈式 Session 管理有兩種⽅案。

Session 複製

部分 Web 服務器能夠⽀持 Session 複製功能,如 Tomcat。⽤戶可以通過修改 Web 服務器的配置⽂件,讓
Web 服務器進⾏ Session 複製,保持每⼀個服務器節點的 Session 數據都能達到⼀致。
這種⽅案的實現依賴於 Web 服務器,需要 Web 服務器有 Session 複製功能。當 Web 應⽤中 Session 數量
較多的時候,每個服務器節點都需要有⼀部分內存⽤來存放 Session,將會佔⽤⼤量內存資源。同時⼤量的
Session 對象通過⽹絡傳輸進⾏複製,不但佔⽤了⽹絡資源,還會因爲複製同步出現延遲,導致程序運⾏錯
誤。
在微服務架構中,往往需要 N 個服務端來共同⽀持服務,不建議採⽤這種⽅案。

Session 集中存儲

在單獨的服務器或服務器集羣上使⽤緩存技術,如 Redis 存儲 Session 數據,集中管理所有的 Session,所
有的 Web 服務器都從這個存儲介質中存取對應的 Session,實現 Session 共享。將 Session 信息從應⽤中
剝離出來後,其實就達到了服務的⽆狀態化,這樣就⽅便在業務極速發展時⽔平擴充。
在微服務架構下,推薦採⽤此⽅案,接下來詳細介紹。

Session 共享

Session

什麼是 Session

由於 HTTP 協議是⽆狀態的協議,因⽽服務端需要記錄⽤戶的狀態時,就需要⽤某種機制來識具體的⽤戶。
Session 是另⼀種記錄客戶狀態的機制,不同的是 Cookie 保存在客戶端瀏覽器中,⽽ Session 保存在服務器
上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在服務器上,這就是 Session
客戶端瀏覽器再次訪問時只需要從該 Session 中查找該客戶的狀態就可以了。
爲什麼需要 Session 共享
在互聯⽹⾏業中⽤戶量訪問巨⼤,往往需要多個節點共同對外提供某⼀種服務,如下圖:
 
⽤戶的請求⾸先會到達前置⽹關,前置⽹關根據路由策略將請求分發到後端的服務器,這就會出現第⼀次的
請求會交給服務器 A 處理,下次的請求可能會是服務 B 處理,如果不做 Session 共享的話,就有可能出現⽤
戶在服務 A 登錄了,下次請求的時候到達服務 B ⼜要求⽤戶重新登錄。
前置⽹關我們⼀般使⽤ lvsNginx 或者 F5 等軟硬件,有些軟件可以指定策略讓⽤戶每次請求都分發到同⼀
臺服務器中,這也有個弊端,如果當其中⼀臺服務 Down 掉之後,就會出現⼀批⽤戶交易失效。在實際⼯作
中我們建議使⽤外部的緩存設備來共享 Session,避免單個節點掛掉⽽影響服務,使⽤外部緩存 Session
後,我們的共享數據都會放到外部緩存容器中,服務本身就會變成⽆狀態的服務,可以隨意的根據流量的⼤
⼩增加或者減少負載的設備。
Spring 官⽅針對 Session 管理這個問題,提供了專⻔的組件 Spring Session,使⽤ Spring Session 在項⽬中
集成分佈式 Session ⾮常⽅便。

Spring Session

Spring Session 提供了⼀套創建和管理 Servlet HttpSession 的⽅案。Spring Session 提供了集羣
SessionClustered Sessions)功能,默認採⽤外置的 Redis 來存儲 Session 數據,以此來解決 Session
享的問題。
Spring Session 爲企業級 Java 應⽤的 Session 管理帶來了⾰新,使得以下的功能更加容易實現:
  • API 和⽤於管理⽤戶會話的實現;
  • HttpSession,允許以應⽤程序容器(即 Tomcat)中性的⽅式替換 HttpSession
  • Session 所保存的狀態卸載到特定的外部 Session 存儲中,如 Redis Apache Geode 中,它們能 夠以獨⽴於應⽤服務器的⽅式提供⾼質量的集羣;
  • ⽀持每個瀏覽器上使⽤多個 Session,從⽽能夠很容易地構建更加豐富的終端⽤戶體驗;
  • 控制 Session ID 如何在客戶端和服務器之間進⾏交換,這樣的話就能很容易地編寫 Restful API,因爲 它可以從 HTTP 頭信息中獲取 Session ID,⽽不必再依賴於 cookieGitChat
  • 當⽤戶使⽤ WebSocket 發送請求的時候,能夠保持 HttpSession 處於活躍狀態。
需要說明的很重要的⼀點就是,Spring Session 的核⼼項⽬並不依賴於 Spring 框架,因此,我們甚⾄能夠將
其應⽤於不使⽤ Spring 框架的項⽬中。
Spring Spring Session Redis 的集成提供了組件:spring-session-data-redis,接下來演示如何使⽤。

快速集成

引⼊依賴包

<dependency>
 <groupId>org.springframework.session</groupId>
 <artifactId>spring-session-data-redis</artifactId>
</dependency>

添加配置⽂件

 
# 數據庫配置
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnico
de=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA 配置
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql= true
# Redis 配置
# Redis 數據庫索引(默認爲0)
spring.redis.database=0 
# Redis 服務器地址
spring.redis.host=localhost
# Redis 服務器連接端⼝
spring.redis.port=6379 
# Redis 服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最⼤連接數(使⽤負值表示沒有限制)
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.shutdown-timeout=100
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
整體配置分爲三塊:數據庫配置、JPA 配置、Redis 配置,具體配置項在前⾯課程都有所介紹。
在項⽬中創建 SessionConfifig 類,使⽤註解配置其過期時間。GitChat

Session 配置:

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class SessionConfig {
}
maxInactiveIntervalInSeconds: 設置 Session 失效時間,使⽤ Redis Session 之後,原 Spring Boot
server.session.timeout 屬性不再⽣效。
僅僅需要這兩步 Spring Boot 分佈式 Session 就配置完成了。

測試驗證

我們在 Web 層寫兩個⽅法進⾏驗證。
@RequestMapping(value = "/setSession")
public Map<String, Object> setSession (HttpServletRequest request){
 Map<String, Object> map = new HashMap<>();
 request.getSession().setAttribute("message", request.getRequestURL());
 map.put("request Url", request.getRequestURL());
 return map;
}
上述⽅法中獲取本次請求的請求地址,並把請求地址放⼊ Key message Session 中,同時結果返回⻚
⾯。
@RequestMapping(value = "/getSession")
public Object getSession (HttpServletRequest request){
 Map<String, Object> map = new HashMap<>();
 map.put("sessionId", request.getSession().getId());
 map.put("message", request.getSession().getAttribute("message"));
 return map;
}
getSession() ⽅法獲取 Session 中的 Session Id Key message 的信息,將獲取到的信息封裝到 Map
並在⻚⾯展示。
 
在測試前我們需要將項⽬ spring-boot-redis-session 複製⼀份,改名爲 spring-boot-redis-session-1 並將端⼝
改爲:9090(server.port=9090)。修改完成後依次啓動兩個項⽬。
 
⾸先訪問 8080 端⼝的服務,瀏覽器輸⼊⽹址 http://localhost:8080/setSession,返
回: {"request Url":"http://localhost:8080/setSession"} ;瀏覽器欄輸⼊⽹址
http://localhost:8080/getSession,返回信息如下:
 
{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}
說明 Url 地址信息已經存⼊到 Session 中。
訪問 9090 端⼝的服務,瀏覽器欄輸⼊⽹址 http://localhost:9090/getSession,返回信息如下:
{"sessionId":"432765e1-049e-4e76-980c-d7f55a232d42","message":"http://localhost:80
80/setSession"}
通過對⽐發現,8080 9090 服務返回的 Session 信息完全⼀致,說明已經實現了 Session 共享。
模擬登錄
在實際中作中常常使⽤共享 Session 的⽅式去保存⽤戶的登錄狀態,避免⽤戶在不同的⻚⾯多次登錄。我們
來簡單模擬⼀下這個場景,假設有⼀個 index ⻚⾯,必須是登錄的⽤戶纔可以訪問,如果⽤戶沒有登錄給出
請登錄的提示。在⼀臺實例上登錄後,再次訪問另外⼀臺的 index 看它是否需要再次登錄,來驗證統⼀登錄
是否成功。
添加登錄⽅法,登錄成功後將⽤戶信息存放到 Session 中。
@RequestMapping(value = "/login")
public String login (HttpServletRequest request,String userName,String password){
 String msg="logon failure!";
 User user= userRepository.findByUserName(userName);
 if (user!=null && user.getPassword().equals(password)){
 request.getSession().setAttribute("user",user);
 msg="login successful!";
 }
 return msg;
}
通過 JPA 的⽅式查詢數據庫中的⽤戶名和密碼,通過對⽐判斷是否登錄成功,成功後將⽤戶信息存儲到
Session 中。
在添加⼀個登出的⽅法,清除掉⽤戶的 Session 信息。
@RequestMapping(value = "/loginout")
public String loginout (HttpServletRequest request){
 request.getSession().removeAttribute("user");
 return "loginout successful!";
}
定義 index ⽅法,只有⽤戶登錄之後纔會看到:index content ,否則提示請先登錄。
 
@RequestMapping(value = "/index")
public String index (HttpServletRequest request){
 String msg="index content";
 Object user= request.getSession().getAttribute("user");
 if (user==null){
 msg="please login first!";
 }
 return msg;
}
和上⾯⼀樣我們需要將項⽬複製爲兩個,第⼆個項⽬的端⼝改爲 9090,依次啓動兩個項⽬。在 test 數據庫
中的 user 表添加⼀個⽤戶名爲 neo,密碼爲 123456 的⽤戶,腳本如下:
INSERT INTO `user` VALUES ('1', '[email protected]', 'smile', '123456', '2018', 'n
eo');
也可以利⽤ Spring Data JPA 特性在應⽤啓動時完成數據初始化:當配置 spring.jpa.hibernate.ddl-auto
: create-drop,在應⽤啓動時,⾃動根據 Entity ⽣成表,並且執⾏ classpath 下的 import.sql
 
⾸先測試 8080 端⼝的服務,直接訪問⽹址 http://localhost:8080/index,返回:please login fifirst!提示請先
登錄。我們將驗證⽤戶名爲 neo,密碼爲 123456 的⽤戶登錄。訪問地址 http://localhost:8080/login?
userName=neo&password=123456 模擬⽤戶登錄,返回:login successful!,提示登錄成功。我們再次訪問
地址 http://localhost:8080/index,返回 index content 說明已經可以查看受限的資源。
 
再來測試 9090 端⼝的服務,直接訪問⽹址 http://localhost:9090/index,⻚⾯返回 index content,並沒有提
示請先進⾏登錄,這說明 9090 服務已經同步了⽤戶的登錄狀態,達到了統⼀登錄的⽬的。
 
我們在 8080 服務上測試⽤戶退出系統,再來驗證 9090 的⽤戶登錄狀態是否同步失效。⾸先訪問地址
http://localhost:8080/loginout 模擬⽤戶在 8080 服務上退出,訪問⽹址 http://localhost:8080/index,返回
please login fifirst!說明⽤戶在 8080 服務上已經退出。再次訪問地址 http://localhost:9090/index,⻚⾯返
回:please login fifirst!,說明 9090 服務上的退出狀態也進⾏了同步。
 
注意,本次實驗只是簡單模擬統⼀登錄,實際⽣產中我們會以 Filter 的⽅式對登錄狀態進⾏校驗,在本
課程的最後⼀節課中也會講到這⽅⾯的內容。
 
我們最後來看⼀下,使⽤ Redis 作爲 Session 共享之後的示意圖:
從上圖可以看出,所有的服務都將 Session 的信息存儲到 Redis 集羣中,⽆論是對 Session 的註銷、更新都
會同步到集羣中,達到了 Session 共享的⽬的。

總結

在微服務架構下,系統被分割成⼤量的⼩⽽相互關聯的微服務,因此需要考慮分佈式 Session 管理,⽅便平
臺架構升級時⽔平擴充。通過向架構中引⼊⾼性能的緩存服務器,將整個微服務架構下的 Session 進⾏統⼀
管理。
Spring Session Spring 官⽅提供的 Session 管理組件,集成到 Spring Boot 項⽬中輕鬆解決分佈式
Session 管理的問題。
 
 
 
 
 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章