使用 Redis 實現 Session 共享

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

在微服務架構中,每每由多個微服務共同支撐前端請求,若是涉及到用戶狀態就須要考慮分佈式 Session 管理問題,好比用戶登陸請求分發在服務器 A,用戶購買請求分發到了服務器 B, 那麼服務器就必須能夠獲取到用戶的登陸信息,不然就會影響正常交易。所以,在分佈式架構或微服務架構下,必須保證一個應用服務器上保存 Session 後,其餘應用服務器能夠同步或共享這個 Session。前端

目前主流的分佈式 Session 管理有兩種方案。mysql

Session 複製git

部分 Web 服務器可以支持 Session 複製功能,如 Tomcat。用戶能夠經過修改 Web 服務器的配置文件,讓 Web 服務器進行 Session 複製,保持每個服務器節點的 Session 數據都能達到一致。github

這種方案的實現依賴於 Web 服務器,須要 Web 服務器有 Session 複製功能。當 Web 應用中 Session 數量較多的時候,每一個服務器節點都須要有一部份內存用來存放 Session,將會佔用大量內存資源。同時大量的 Session 對象經過網絡傳輸進行復制,不但佔用了網絡資源,還會由於複製同步出現延遲,致使程序運行錯誤。redis

在微服務架構中,每每須要 N 個服務端來共同支持服務,不建議採用這種方案。spring

Session 集中存儲sql

在單獨的服務器或服務器集羣上使用緩存技術,如 Redis 存儲 Session 數據,集中管理全部的 Session,全部的 Web 服務器都從這個存儲介質中存取對應的 Session,實現 Session 共享。將 Session 信息從應用中剝離出來後,其實就達到了服務的無狀態化,這樣就方便在業務極速發展時水平擴充。數據庫

在微服務架構下,推薦採用此方案,接下來詳細介紹。後端

1.1    Session 共享

1.1.1  Session

什麼是 Session瀏覽器

因爲 HTTP 協議是無狀態的協議,於是服務端須要記錄用戶的狀態時,就須要用某種機制來識具體的用戶。Session 是另外一種記錄客戶狀態的機制,不一樣的是 Cookie 保存在客戶端瀏覽器中,而 Session 保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在服務器上,這就是 Session。客戶端瀏覽器再次訪問時只須要從該 Session 中查找該客戶的狀態就能夠了。

爲何須要 Session 共享

在互聯網行業中用戶量訪問巨大,每每須要多個節點共同對外提供某一種服務,以下圖:

 

用戶的請求首先會到達前置網關,前置網關根據路由策略將請求分發到後端的服務器,這就會出現第一次的請求會交給服務器 A 處理,下次的請求可能會是服務 B 處理,若是不作 Session 共享的話,就有可能出現用戶在服務 A 登陸了,下次請求的時候到達服務 B 又要求用戶從新登陸。

前置網關咱們通常使用 lvs、Nginx 或者 F5 等軟硬件,有些軟件能夠指定策略讓用戶每次請求都分發到同一臺服務器中,這也有個弊端,若是當其中一臺服務 Down 掉以後,就會出現一批用戶交易失效。在實際工做中咱們建議使用外部的緩存設備來共享 Session,避免單個節點掛掉而影響服務,使用外部緩存 Session 後,咱們的共享數據都會放到外部緩存容器中,服務自己就會變成無狀態的服務,能夠隨意的根據流量的大小增長或者減小負載的設備。

Spring 官方針對 Session 管理這個問題,提供了專門的組件 Spring Session,使用 Spring Session 在項目中集成分佈式 Session 很是方便。

1.1.2  Spring Session

Spring Session 提供了一套建立和管理 Servlet HttpSession 的方案。Spring Session 提供了集羣 Session(Clustered 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,而沒必要再依賴於 cookie;
  • 當用戶使用 WebSocket 發送請求的時候,可以保持 HttpSession 處於活躍狀態。

須要說明的很重要的一點就是,Spring Session 的核心項目並不依賴於 Spring 框架,所以,咱們甚至可以將其應用於不使用 Spring 框架的項目中。

Spring 爲 Spring Session 和 Redis 的集成提供了組件:spring-session-data-redis,接下來演示如何使用。

1.1.3  快速集成

引入依賴包

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

添加配置文件

# 數據庫配置
testtruetruespring.datasource.url=jdbc:mysql://localhost:3306/?serverTimezone=UTC&useUnicode=&characterEncoding=utf-8&useSSL=
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
truespring.jpa.show-sql=
# 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 配置,具體配置項在前面課程都有所介紹。

在項目中建立 SessionConfig 類,使用註解配置其過時時間。

Session 配置:

@Configuration
@EnableRedisHttpSession8640030(maxInactiveIntervalInSeconds =*)
publicclass SessionConfig {
}

maxInactiveIntervalInSeconds: 設置 Session 失效時間,使用 Redis Session 以後,原 Spring Boot 中的 server.session.timeout 屬性再也不生效。

僅僅須要這兩步 Spring Boot 分佈式 Session 就配置完成了。

1.1.4  測試驗證

咱們在 Web 層寫兩個方法進行驗證。

"/setSession"@RequestMapping(value =)
MapStringObjectpublic<,> setSession (HttpServletRequest request){
MapStringObjectnew   <,> map =HashMap<>();
"message"    request.getSession().setAttribute(, request.getRequestURL());
"request Url"    map.put(, request.getRequestURL());
return   map;
}

上述方法中獲取本次請求的請求地址,並把請求地址放入 Key 爲 message 的 Session 中,同時結果返回頁面。

"/getSession"@RequestMapping(value =)
public Object getSession (HttpServletRequest request){
mapnew    Map<String, Object>=HashMap<>();
map"sessionId"   .put(, request.getSession().getId());
map"message""message"   .put(, request.getSession().getAttribute());
returnmap   ;
}

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:8080/setSession"{:,:}

說明 Url 地址信息已經存入到 Session 中。

訪問 9090 端口的服務,瀏覽器欄輸入網址 http://localhost:9090/getSession,返回信息以下:

"sessionId""432765e1-049e-4e76-980c-d7f55a232d42""message""http://localhost:8080/setSession"{:,:}

經過對比發現,8080 和 9090 服務返回的 Session 信息徹底一致,說明已經實現了 Session 共享。

1.1.5  模擬登陸

在實際中做中經常使用共享 Session 的方式去保存用戶的登陸狀態,避免用戶在不一樣的頁面屢次登陸。咱們來簡單模擬一下這個場景,假設有一個 index 頁面,必須是登陸的用戶才能夠訪問,若是用戶沒有登陸給出請登陸的提示。在一臺實例上登陸後,再次訪問另一臺的 index 看它是否須要再次登陸,來驗證統一登陸是否成功。

添加登陸方法,登陸成功後將用戶信息存放到 Session 中。

value"/login"@RequestMapping(=)
public String login (HttpServletRequest request,String userName,String password){
"logon failure!"    String msg=;
    User user= userRepository.findByUserName(userName);
ifnullequals   (user!=&& user.getPassword().(password)){
"user"        request.getSession().setAttribute(,user);
"login successful!"        msg=;
    }
return   msg;
}

經過 JPA 的方式查詢數據庫中的用戶名和密碼,經過對比判斷是否登陸成功,成功後將用戶信息存儲到 Session 中。

在添加一個登出的方法,清除掉用戶的 Session 信息。

value"/loginout"@RequestMapping(=)
public String loginout (HttpServletRequest request){
"user"    request.getSession().removeAttribute();
return"loginout successful!"   ;
}

定義 index 方法,只有用戶登陸以後纔會看到:index content ,不然提示請先登陸。

value"/index"@RequestMapping(=)
public String index (HttpServletRequest request){
"index content"    String msg=;
"user"    Object user= request.getSession().getAttribute();
ifnull   (user==){
"please login first!"        msg=;
    }
return   msg;
}

和上面同樣咱們須要將項目複製爲兩個,第二個項目的端口改成 9090,依次啓動兩個項目。在 test 數據庫中的 user 表添加一個用戶名爲 neo,密碼爲 123456 的用戶,腳本以下: 

INSERTINTO`user`VALUES'1''ityouknow@126.com''smile''123456''2018''neo'(,,,,,);

也能夠利用 Spring Data JPA 特性在應用啓動時完成數據初始化:當配置 spring.jpa.hibernate.ddl-auto : create-drop,在應用啓動時,自動根據 Entity 生成表,而且執行 classpath 下的 import.sql。

首先測試 8080 端口的服務,直接訪問網址 http://localhost:8080/index,返回:please login first!提示請先登陸。咱們將驗證用戶名爲 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 first!說明用戶在 8080 服務上已經退出。再次訪問地址 http://localhost:9090/index,頁面返回:please login first!,說明 9090 服務上的退出狀態也進行了同步。

注意,本次實驗只是簡單模擬統一登陸,實際生產中咱們會以 Filter 的方式對登陸狀態進行校驗,在本課程的最後一節課中也會講到這方面的內容。

咱們最後來看一下,使用 Redis 做爲 Session 共享以後的示意圖:

 

從上圖能夠看出,全部的服務都將 Session 的信息存儲到 Redis 集羣中,不管是對 Session 的註銷、更新都會同步到集羣中,達到了 Session 共享的目的。

1.2    總結

在微服務架構下,系統被分割成大量的小而相互關聯的微服務,所以須要考慮分佈式 Session 管理,方便平臺架構升級時水平擴充。經過向架構中引入高性能的緩存服務器,將整個微服務架構下的 Session 進行統一管理。

Spring Session 是 Spring 官方提供的 Session 管理組件,集成到 Spring Boot 項目中輕鬆解決分佈式 Session 管理的問題。

點擊這裏下載源碼

相關文章
相關標籤/搜索