分佈式 session 的幾種解決方案,你中意哪種?
我發現了一個商城,我還沒有登錄,就可以往購物車中添加商品,加了好幾件後,我準備付款,需要我先去登錄,登錄完之後付款。
現在很多商城,都會要求用戶先去登錄,登錄之後再往購物車中添加商品,這樣用戶、購物車、商品,三個對象之間就有了綁定關係。
而針對我最開始說的那種情況,其實就是基於session
做的,客戶端往購物車中添加第一個商品的時候,發送一個請求,服務器收到請求之後,創建session
,然後返回當前session
對應的一個JessionId
,瀏覽器存儲在cookie
中,客戶端往購物車添加第二個商品時,攜帶JessionId
,服務端收到請求後,更新session
。瀏覽器關閉後,cookie
失效,JessionId
也就丟失了,需要重新往購物車中添加商品,默認情況下,session
有效期爲30
分鐘。
在分佈式環境下,session
就會出現問題了,假如服務端部署在兩個服務器A
和B
上。第一次往購物車添加商品時,請求落在了服務器 A 上,服務器 A 創建了一個session
,並返回JessionId
,第二次往購物車添加商品時,請求落在了服務器 B 上,請求攜帶的JesssionId
在服務器 B 上並不會找到對應的session
。這時候服務器 B 就會創建一個新的session
,並返回對應的JessionId
,客戶端發現第一次添加的商品丟失了。。。
接下來,一起來學習分佈式環境下session
一致性是如何實現的。
一、客戶端存儲
既然分佈式環境中,一個客戶端的多個請求可能會落在多個服務器上,那麼我們是否可以改變策略,直接將 session 信息存儲在客戶端?可以的,服務器將 session 信息直接存儲到 cookie 中,這樣就保證了 session 的一致性,但是並不推薦這樣去做,因爲將一些信息存儲在 cookie 中,相當於就把這些信息暴露給了客戶端,存在嚴重的安全隱患。
缺點:
-
安全性存在問題
-
cookie 對於數據類型及數據大小有所限制
二、session 複製
將服務器 A 的 session,複製到服務器 B,同樣將服務器 B 的 session 也複製到服務器 A,這樣兩臺服務器的 session 就一致了。像 tomcat 等 web 容器都支持 session 複製的功能,在同一個局域網內,一臺服務器的session
會廣播給其他服務器。
缺點:
同一個網段內服務器太多,每個服務器都會去複製 session,會造成服務器內存浪費。
三、session 黏性
利用Nginx
服務器的反向代理,將服務器 A 和服務器 B 進行代理,然後採用ip_hash
的負載策略,將客戶端和服務器進行綁定,也就是說客戶端 A 第一次訪問的是服務器 B,那麼第二次訪問也必然是服務器 B,這樣就不存在 session 不一致的問題了。
缺點:
如果服務器 A 宕機了,那麼客戶端 A 和客戶端 B 的 session 就會出現丟失。
四、session 集中管理
這種方式就是將所有服務器的session
進行統一管理,可以使用redis
等高性能服務器來集中管理 session,而且 spring 官方提供的spirng-session
就是這樣處理session
的一致性問題。這也是目前企業開發用到的比較多的一種分佈式session
解決方案。
五、spring-session 實戰
Spring
提供了處理分佈式 session 的解決方案——Spring Session
。Spring Session
提供了用於管理用戶會話的 API 和實現。
Spring Session
提供了對redis
,mongodb
,mysql
等常用的存儲庫的支持,Spring Session
提供與HttpSession
的透明整合,這意味着開發人員可以使用 Spring Session 支持的實現切換HttpSession
實現。還是原來的配方,產生了不一樣的味道!
Spring Session
添加了一個SessionRepositoryFilter
的過濾器,用來修改包裝請求和響應,包裝後的請求爲SessionRepositoryRequestWrapper
,調用getSession()
方法的時候實際上就是調用Spring Session
實現了的 session。
Spring Session
使用非常簡單,添加了相關依賴後,直接操作HttpSession
就可以實現效果。
第一步:添加Spring Session
和 redis
的相關依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
第二步:配置 redis 相關信息
spring:
redis:
# redis庫
database: 0
# redis 服務器地址
host: localhost
# redis 端口號
port: 6379
# redis 密碼
password:
# session 使用redis存儲
session:
store-type: redis
第三步:項目中使用 session
public String sessionTest(HttpServletRequest request){
HttpSession session = request.getSession();
session.setAttribute("key","value");
return session.getAttribute("key").toString();
}
redis
中每個 session 存儲了三條信息。
-
第一個存儲這個 Session 的 id,是一個 Set 類型的 Redis 數據結構。這個 k 中的最後的 1439245080000 值是一個時間戳,根據這個 Session 過期時刻滾動至下一分鐘而計算得出。
-
第二個用來存儲 Session 的詳細信息,包括 Session 的過期時間間隔、最近的訪問時間、attributes 等等。這個 k 的過期時間爲 Session 的最大過期時間 + 5 分鐘。如果默認的最大過期時間爲 30 分鐘,則這個 k 的過期時間爲 35 分鐘。
-
第三個用來表示 Session 在 Redis 中的過期,這個 k-v 不存儲任何有用數據,只是表示 Session 過期而設置。這個 k 在 Redis 中的過期時間即爲 Session 的過期時間間隔。
處理一個 session 爲什麼要存儲三條數據,而不是一條呢!對於 session 的實現,需要監聽它的創建、過期等事件,redis 可以監聽某個 key 的變化,當 key 發生變化時,可以快速做出相應的處理。
但是 Redis 中帶有過期的 key 有兩種方式:
-
當訪問時發現其過期
-
Redis 後臺逐步查找過期鍵
當訪問時發現其過期,會產生過期事件,但是無法保證 key 的過期時間抵達後立即生成過期事件。
spring-session 爲了能夠及時的產生 Session 的過期時的過期事件,所以增加了:
spring:session:sessions:expires:726de8fc-c045-481a-986d-f7c4c5851a67``spring:session:expirations:1620393360000
spring-session 中有個定時任務,每個整分鐘都會查詢相應的 spring:session:expirations: 整分鐘的時間戳中的過期 SessionId,然後再訪問一次這個 SessionId,即 spring:session:sessions:expires:SessionId,以便能夠讓 Redis 及時的產生 key 過期事件——即 Session 過期事件。
參考
https://www.cnblogs.com/sxw123/p/13803478.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Ps-uS7rw828SdsuAaZoRAg