SpringSession+Redis實現叢集會話共享

atd681發表於2018-08-13

0) 前言

WEB應用開發完成後部署到Tomcat或其他容器中供使用者訪問. 小型應用在一臺伺服器上安裝Tomcat並部署WEB應用. 隨著訪問量增大, Tomcat的壓力會越來越大, 直至崩潰. 為了保證WEB應用的承載能力, 需要對WEB應用進行叢集處理.

技術發展到今天, 叢集/負載均衡已經變的相對簡單了. 下面用通俗的語言給剛入門的同學介紹下這兩個概念:

某KFC開業時只有一個點餐視窗(一臺Tocmat伺服器, 可以節約成本)對外提供點餐服務. 應對日常點餐沒有問題, 當飯口或者週末時一個視窗就會排起長隊(高併發). 不僅顧客有怨言(請求響應時間長, 使用者體驗差), 服務員也會很累, 終於有一天他累倒了(Tomcat掛掉了).

這時在側面增加了一個視窗(增加一臺Tomcat伺服器)提供點餐服務, 但是很多顧客不知道新視窗, 依舊在原有視窗排起了長隊(使用者依舊訪問原來的Tomcat), 這時需要有一個人專門站在門口根據每個視窗的排隊情況指引顧客去哪個視窗點餐(負載均衡器). 這個人作用是為了讓各個視窗的點餐人數大致相等, 避免有的視窗很忙, 有的很閒. 隨著顧客增加, 點餐視窗也會相應增加(Tomcat越來越多).

  • 叢集: 一群伺服器集合在一起提供服務, 上例中多個點餐視窗(多臺Tomcat)共同提供點餐服務就是叢集.
  • 負載均衡: 讓叢集中每個點餐視窗(每個Tomcat)的負載情況保持均衡, 不要出現某一個或幾個太空閒.

兩個概念是同時出現的, 沒有叢集的服務(單一Tomcat)也不存在負載均衡之說, 叢集的服務沒有負載均衡會浪費資源.

WEB負載均衡方案很多, Nginx+Tomcat是常用的方案之一. Nginx作為負載均衡器根據每個Tomcat的負載情況進行分流.

SpringSession+Redis實現叢集會話共享

  • 每個Tomcat都相當於點餐視窗, 都可以提供點餐服務
  • 每次要點餐都得先經過Nginx
  • Nginx會根據每個視窗的空閒情況進行分配使用者去哪個視窗點餐
  • 第一次在1號視窗點餐, 點完後馬上再次點餐, 有可能被分配到2號視窗

下面我們搭建負載均衡的WEB應用

1) 搭建WEB應用

準備WEB應用, 用兩個Tomcat部署, 測試時為了能夠區分請求是由哪個Tomcat進行處理, 將Tomcat埠號作為結果返回.

/**
 * 獲取部署專案的Tomcat埠號
 */
@RequestMapping("/port/get")
@ResponseBody
public String getPort(HttpServletRequest request) {
    return String.valueOf(request.getLocalPort());
}
複製程式碼

本例中分別使用5677, 5688兩個埠部署該專案. 訪問/port/get請求返回結果為Tomcat的埠號

  • http:// localhost:5677/port/get : 返回5677
  • http:// localhost:5688/port/get : 返回5688

2) Nginx配置負載均衡

Window下Nginx安裝比較簡單, 不會安裝的同學自行百度, 簡單介紹下Nginx配置檔案: nginx.conf

# Nginx程式數
worker_processes  1;
 
events {
    # 最大併發連結數
    worker_connections  1024;
}
 
# Nginx處理HTTP請求相關的配置
# http不能重複, 全域性唯一
http {
 
    # 虛擬主機, 可配置多個虛擬主機
    # Nginx監聽88,89,90三個埠, 可配置三個server
    server {
        # 埠號, 訪問88埠會都按照該server下的配置進行處理
        listen       88;
        # 主機名稱
        server_name  localhost;
        # 根據正規表示式匹配URL, 匹配到的URL按照該location下的配置進行處理
        # /代表訪問88埠的所有請求
        location / {
            # 靜態資源所在根目錄, 會從該目錄下查詢靜態資源
            # 例: 訪問/a.html, 找到D:/a.html並返回
            root  D:/;
        }
    }
 
}
複製程式碼

上述配置檔案最基礎的Nginx配置, 當我們訪問http://localhost:88時會由Nginx處理, 下面我們配置Nginx的負載均衡.

  • 配置1)中定義的兩個tomcat, 在http節點下新增如下程式碼:
# 定義需要進行負載均衡的伺服器資訊
# upstream為關鍵字, springsession為自定義的名稱
# server為關鍵字, 代表一個服務或服務(一個tomcat)
# server的內容為伺服器的資訊, 形式為ip:埠
# weight定義了伺服器負載的權重, 每4次請求有3次轉發到5688, 1次到5677
upstream springsession { 
    server localhost:5677 weight=1; 
    server localhost:5688 weight=3; 
}
複製程式碼
  • 配置當訪問Nginx的所有請求轉發至兩個伺服器處理
location / {
    # root  D:/;
    # 轉發至名稱為springsession的upstream處理
    proxy_pass http://springsession; 
}
複製程式碼

3) 測試負載均衡

訪問http://localhost:88/port/get, Nginx將請求轉發至兩臺tomcat中的一個進行處理. 可以發現請求返回的結果是不一樣的

SpringSession+Redis實現叢集會話共享

  • 根據配置的權重, 每4次訪問有3次由5688上, 1次由5677處理.
  • 權重配置只是最終平均值為3/4和1/4, 不一定是前三次訪問都會由5688處理.
  • 不配置weight時, 一次請求兩個tomcat被分配到的概率各佔50%

負載均衡配置好了, 有這樣一個問題:

你在1號視窗點餐時把鑰匙暫存到該視窗, 下次在點餐可能被分配到2號視窗或其他視窗(也有可能分配到1號視窗), 那麼在其他視窗取鑰匙顯然是行不通的. 因為其他視窗沒有你的鑰匙. 這時你只能祈禱能快速把你分配到1號視窗.

如果儲存鑰匙的操作變為在SESSION中儲存資訊, 那麼當你的請求被Tomcat1處理時, Tomcat1會為你生成一個SESSION, 你在SESSION裡面設定了資訊, 下次你的請求被分配到Tomcat2處理, Tomcat2又會為你生成一個SESSION. 這是兩個獨立的並且不共享的SESSION. 因此你是不可能的在Tomcat2中獲取你在Tomcat1中儲存的資訊.

登入的原理其實就是在SESSION中儲存登入狀態, 按照上面的分析, 登入在叢集部署的服務中就失效了. 在Tomcat1中登入, 下次訪問Tomcat2, 此時通過SESSION判斷登入狀態得到的一定是未登入, 還需要再次登入. 使用者瘋, 你瘋, 老闆也會瘋...


如果有一個公用位置用來存放東西, 所有的點餐視窗都在公用位置存取顧客物品, 上面的問題就迎刃而解了.

這就是本文重點: 統一管理叢集下各WEB應用的SESSION.

  • 容器的選擇: 我們需要一個能夠統一存放SESSION的容器. 從以下3點分析, Redis無疑是最合適的. SESSION是經常被讀取的, 因此資料庫, 檔案系統均不適合, 最好是從記憶體操作. SESSION是有ID的, 一個ID對應一個SESSION, 最好是一個K/V的容器 SESSION是有時效性的(時間長不用, 需要刪除). 最好能夠設定過期時間

  • SESSION存取機制: 由於SESSION是Tomcat生成的, 因此首先想到的是修改Tomcat的SESSION機制, 從Redis中存取SESSION, 這樣會帶來一個問題, 假設Tocmat升級了, 我們還需要重新對Tomcat進行修改. 因此這個方案可行性較差. 我們可以這樣考慮, 即使Tomcat生成了SESSION, 我們也是在WEB應用中使用, 為什麼不在WEB應用中重新生成一個SESSION呢, 編寫這樣一個過濾器, 在進入WEB應用之前, 拋棄Tomcat的SESSION, 從Redis中獲取SESSION.


恰巧有這樣一個框架幫助我們完成上面的想法, 只需要配置一下即可實現統一管理SESSION. 他就是Spring Session.

為了對Spring Session的功能印象深刻, 我們先來測試一下沒有Spring Session時我們的叢集應用是怎樣處理SESSION的

把我們負載均衡的WEB應用中增加一個控制器方法, 把每次的SESSION ID輸出一下.

/**
  * 獲取部署專案的SESSION ID
  */
@RequestMapping("/sessionid/get")
@ResponseBody
public String getPort(HttpServletRequest request, HttpSession session) {
    int port = request.getLocalPort(); // 埠
    String sessionId = request.getSession().getId(); // SESSION ID
 
    return "port: " + port + ", session id: " + sessionId;
}
複製程式碼

啟動專案, 多次訪問http://localhost:88/sessionid/get

SpringSession+Redis實現叢集會話共享

  • 兩次訪問都在同一Tomcat下的SESSOIN ID是不變的
  • 兩次訪問在不同的Tomcat下的SESSION ID是變化的
  • 訪問不同的Tomcat後, 再次訪問同一Tomcat時SESSION ID也是變化的

出現上述情況的原因如下:

  1. 訪問5677, 由於沒有SESSION, Tomcat5677生成SESSION, ID為1, 並將1返回客戶端
  2. 訪問5677, 瀏覽器攜帶SESSION_ID=1, Tomcat5677找到對應的SESSION. 因此SESSION_ID為1
  3. 訪問5688, 瀏覽器攜帶SESSION_ID=1, Tomcat5688找不到對應的SESSION, 重新生成SESSION, ID為2, 並將2返回客戶端
  4. 訪問5677, 瀏覽器攜帶SESSION_ID=2, Tomcat5677找不到對應的SESSION, 重新生成SESSION, ID為3, 並將3返回客戶端
  5. 訪問5688, 瀏覽器攜帶SESSION_ID=3, Tomcat5688找不到對應的SESSION, 重新生成SESSION, ID為4, 並將4返回客戶端

4) 統一SESSION管理

下面我們來用Spring Session來管理WEB應用的SESSION

1) 安裝Redis並開啟

參見文章http://www.cnblogs.com/jaign/articles/7920588.html

2) 新增Spring Session依賴

// Spring Session依賴
"org.springframework.session:spring-session-data-redis:2.0.5.RELEASE",
// Redis依賴
"io.lettuce:lettuce-core:5.0.4.RELEASE"
複製程式碼

3) 配置Spring Session過濾器

Web.xml中配置Spring Session提供的過濾器, 該過濾器主要負責將Tomcat生成的SESSION替換成Redis中儲存的SESSION.

<!-- Spring Session過濾器 -->
<!-- 負責在進入WEB應用之前將Tomcat生成的SESSION替換為Redis中的SESSION -->
<filter>
	<filter-name>springSessionRepositoryFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSessionRepositoryFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
複製程式碼

4) SpringSession/Redis配置

在Spring配置檔案中增加Spring Session配置和Redis配置

beans {
 
    xmlns context: "http://www.springframework.org/schema/context"
 
    // 啟動註解方式
    context.'annotation-config'()
 
    // 配置Spring Session
    // 實際上是配置Web.xml中使用的Spring Session過濾器
    // 將Tomcat的Session替換為Redis中管理的Session
    sessionConfig(RedisHttpSessionConfiguration)
 
    // 配置Redis客戶端連線
    // 預設連線本地6379埠
    lettuce(LettuceConnectionFactory)
 
}
複製程式碼

5) 測試

啟動專案, 多次訪問http://localhost:88/sessionid/get, 無論如何訪問SESSION ID都是一樣的.

SpringSession+Redis實現叢集會話共享

同時Redis中也出現了當前SESSION的記錄.

SpringSession+Redis實現叢集會話共享

使用Spring Session後訪問叢集下的WEB應用時SESSION處理過程:

  1. 訪問5677, 由於Redis中沒有SESSION, 因此會生成一個SESSION並存入Redis, ID為1, 並將1返回客戶端
  2. 訪問5677, 瀏覽器攜帶SESSION_ID=1, Tomcat5677Redis中找到了SESSION. 因此SESSION_ID1
  3. 訪問5688, 瀏覽器攜帶SESSION_ID=1, Tomcat5688Redis中找到了SESSION. 因此SESSION_ID1
  4. 清除Redis, 再次訪問5677, 由於Redis中沒有ID為1SESSION, 因此會重新生成, ID也相應變化了

5) 示例程式碼

此時我們已經實現了統一管理SESSION, 無論訪問任一TOMCAT都可以找到相同的SESSION.

當我們的應用進行叢集后, 統一管理SESSION勢在必行, 實現統一管理SESSION的方式很多, 本文只是其中一種方式. 重在讓同學們理解統一管理SESSION的重要性和他的基本原理.

  • 示例程式碼地址: https://github.com/atd681/alldemo
  • 示例專案名稱: atd681-springsession

相關文章