使用Spring Session實現Spring Boot水平擴充套件

博文視點發表於2017-12-01

本文使用Spring Session實現了Spring Boot水平擴充套件,每個Spring Boot應用與其他水平擴充套件的Spring Boot一樣,都能處理使用者請求。如果當機,Nginx會將請求反向代理到其他執行的Spring Boot應用上,如果系統需要增加吞吐量,只需要再啟動更多的Spring Boot應用即可。

  Spring Boot應用通常會部署在多個Web伺服器上同時提供服務,這樣做有很多好處:

  單個應用當機不會停止服務,升級應用可以逐個升級而不必停止服務。

  提高了應用整體的吞吐量。

  我們稱這種部署方式為水平擴充套件,前端通過Nginx提供反向代理,會話管理可以通過Spring Session,使用Redis來存放Session。部署Spring Boot應用到任意一臺Web伺服器上,從而提高了系統可靠性和可伸縮性。

1 水平擴充套件實現

  當系統想提升處理能力的時候,通常用兩種選擇,一種是重置擴充套件架構,即提升現有系統硬體的處理能力,比如提高CPU頻率、使用更好的儲存器。另外一種選擇是水平擴充套件架構,即部署系統到更多的伺服器上同時提供服務。這兩種方式各有利弊,現在通常都優先採用水平擴充套件架構,這是因為:

重置擴充套件架構

  缺點:架構中的硬體提升能力有限,而且硬體能力提升往往需要更多的花銷;

  優點:應用系統不需要做任何改變。

水平擴充套件

  優點:成本便宜;

  缺點:更多的應用導致管理更加複雜。對於Spring Boot 應用,會話管理是一個難點。
Spring Boot 應用水平擴充套件有兩個問題需要解決,一個是將使用者的請求派發到水平部署的任意一臺Spring Boot應用,通常用一個反向代理伺服器來實現,本文將使用Nginx作為反向代理伺服器。

  反向代理(Reverse Proxy)方式是指接收internet上的連線請求,然後將請求轉發給內部網路上的伺服器,並將從伺服器上得到的結果返回給internet上請求連線的客戶端,此時代理伺服器對外就表現為一個反向代理伺服器。

  正向代理伺服器:區域網內通過一個正向代理伺服器訪問外網。

  另外一個需要解決的問題是會話管理, 單個Spring Boot應用的會話由Tomcat來管理,會話資訊與Tomcat存放在一起。如果部署多個Spring Boot應用,對於同一個使用者請求,即使請求通過Nginx派發到不同的Web伺服器上,也能共享會話資訊。有兩種方式可以實現。

  複製會話:Web伺服器通常都支援Session複製,一臺應用的會話資訊改變將立刻複製到其他叢集的Web伺服器上。

  集中式會話:所有Web伺服器都共享一個會話,會話資訊通常存放在一臺伺服器上,本文使用Redis伺服器來存放會話。

  複製會話的缺點是每次會話改變需要複製到多臺Web伺服器上,效率較低。因此Spring Boot應用採用第二種方式(集中式會話方式),結構如下圖所示。

  上圖是一個大型分散式系統架構,包含了三個獨立的子系統。業務子系統一和業務子系統二分別部署在一臺Tomcat伺服器上,業務子系統三部署在兩臺Tomcat伺服器上,採用水平擴充套件。

  架構採用Nginx作為反向代理,其後的各個子系統都採用Spring Session,將會話存放在Redis中,因此,這些子系統雖然是分開部署的,支援水平擴充套件,但能整合成一個大的系統。Nginx提供統一的入口,對於使用者訪問,將按照某種策略,比如根據訪問路徑派發到後面對應的Spring Boot應用中,Spring Boot呼叫Spring Session取得會話資訊,Spring Session並沒有從本地存取會話,會話資訊存放在Redis伺服器上。

2 Nginx的安裝和配置

  Nginx是一款輕量級的Web 伺服器/反向代理伺服器及電子郵件(IMAP/POP3)、TCP/UDP代理伺服器,並在一個BSD-like協議下發行。由俄羅斯的程式設計師Igor Sysoev開發,供俄國大型的入口網站及搜尋引擎Rambler使用。其特點是佔有記憶體少,併發能力強,事實上Nginx的併發能力確實在同型別的網頁伺服器中表現較好,國內使用Nginx的網站有百度、新浪、網易、騰訊等。

2.1 安裝Nginx

  開啟Nginx網站(http://nginx.org/ ),進入下載頁面,根據自己的作業系統選擇下載,以Windows系統為例,下載nginx/Windows-1.11.10版本,直接解壓,然後執行Nginx即可。

  如果是Mac,可以執行:

>brew install nginx

  Nginx預設會安裝在/usr/local/Cellar/nginx/目錄下,配置檔案在/usr/local/etc/nginx/nginx.conf目錄下,日誌檔案在 /usr/local/var/log/nginx/目錄下。

  以下是Nginx的常用命令:

  nginx,啟動Nginx,預設監聽80埠。

  nginx -s stop,快速停止伺服器。

  nginx -s quit,停止伺服器,但要等到請求處理完畢後關閉。

  nginx -s reload,重新載入配置檔案。

  Nginx啟動後,可以訪問http://127.0.0.1:80 ,會看到Nginx的歡迎頁面,如下圖所示。

  如果80埠訪問不了,則可能是因為你下載的版本的原因,Nginx的HTTP埠配置成其他埠,編輯conf/nginx.conf,找到:

server {
listen       80;
}

  修改listen引數到80埠即可。

  Nginx的log目錄下提供了三個檔案:

  access.log,記錄了使用者的請求資訊和響應。

  error.log,記錄了Nginx執行的錯誤日誌。

  nginx.pid,包含了Nginx的程式號。

2.2 配置Nginx

  Nginx的配置檔案conf/nginx.conf下包含多個指令塊,我們主要關注http塊和location塊。

  http塊:可以巢狀多個Server,配置代理、快取、日誌定義等絕大多數功能和第三方模組,如mime-type定義、日誌自定義、是否使用sendfile傳輸檔案、連線超時時間、單連線請求數等。

  location塊:配置請求的路由,以及各種頁面的處理情況。
由於本文主要是講水平擴充套件Spring Boot應用,因此,我們需要在http塊中增加upstream指令,內容如下:

http {
upstream backend {   
    server 127.0.0.1:9000;
    server 127.0.0.1:9001
}
}

  backend也可以為任意名字,我們在下面的配置將要引用到:

location / {
    proxy_pass http://backend;  
}

  location後可以是一個正規表示式,我們這裡用“/”表示所有客戶端請求都會傳給http:// backend,也就是我們配置的backend指令的地址列表。因此,整個http塊類似下面的樣子:

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    upstream backend {   
    server 127.0.0.1:9000;
    server 127.0.0.1:9001;
    }
    server {
        listen       80;
        server_name  localhost;

        location / {
            proxy_pass http://backend;  
        }      
    }
}

  我們在後面將建立一個Spring Boot應用,並分別以9000和9001兩個埠啟動,然後在Spring Session的基礎上一步步來完成Spring Boot應用的水平擴充套件。

  注意:Nginx反向代理預設情況下會輪詢後臺應用,還有一種配置是設定ip_hash,這樣,固定客戶端總是反向代理到後臺的某一個伺服器。這種設定方式就不需要使用Spring Session來管理會話,使用Tomcat的會話管理即可。但弊端是如果伺服器當機或者因為維護重啟,則會話丟失。ip_hash設定如下:

upstream backend { 
ip_hash;
server 127.0.0.1:9000;
server 127.0.0.1:9001
}

3 Spring Session

3.1 Spring Session介紹

  在預設情況下,Spring Boot使用Tomcat伺服器的Session實現,我們編寫一個例子用於測試:

@Controller
public class SpringSessionCrontroller {

    Log log = LogFactory.getLog(SpringSessionCrontroller.class);

    @RequestMapping("/putsession.html") 
    public @ResponseBody String putSession(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info(session.getClass());
        log.info(session.getId());
        String name = "xiandafu";
        session.setAttribute("user", name);
        return "hey,"+name;
    }
}

  如果訪問服務/putsession.html,控制檯輸出為:

SpringSessionCrontroller       : class  org.apache.catalina.session.StandardSessionFacade
SpringSessionCrontroller       : F567C587EA25CBD5B9A75C62AB51904D

  可以看到,Session管理是通過Tomcat提供的org.apache.catalina.session.StandardSessionFacade實現的。

  在配置檔案application.properties中新增如下內容:

spring.session.store-type=Redis|JDBC|Hazelcast|none

  Spring Boot配置很容易切換到不同的Session管理方式,總共有以下幾種:

  Redis,Session資料存放Redis中。

  JDBC,會話資料存放在資料庫中,預設情況下SPRING_SESSION表存放Session基本資訊,如sessionId、建立時間、最後一次訪問時間等,SPRING_SESSION_ ATTRIBUTES存放了session資料,ATTRIBUTE_NAME列儲存了Session的Key,ATTRIBUTE_BYTES列以位元組形式儲存了Session的Value,Spring Session會自動建立這兩張表。

  Hazelcast,Session資料存放到Hazelcast。

  None,禁用Spring Session功能。

  通過配置屬性spring.session.store-type來指定Session的儲存方式,如:

spring.session.store-type=Redis

  修改為配置和增加Spring Session依賴後,如果訪問服務/putsession.html,控制檯輸出為:

SpringSessionCrontroller       : class org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper

SpringSessionCrontroller       : d4315e92-48e1-4a77-9819-f15df9361e68

  可以看到,Session已經替換為HttpSessionWrapper實現,這個類負責Spring Boot 的Session儲存型別的具體實現。

3.2 使用Redis

  本將用Redis來儲存Session,你需要安裝Redis,如未安裝,請參考《Spring Boot 2精髓:從構建小系統到架構分散式大系統》中Redis一章,Spring Boot的配置如下:

spring.session.store-type=Redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=Redis!123

  還需要引入對Redis的依賴:

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

  再次訪問/putsession.html後,我們通過Redis客戶端工具訪問Redis,比如使用redis-cli,輸入如下命令:

keys spring:session:*

  查詢所有“spring:session:”開頭的keys,輸出如下:

3) "spring:session:sessions:expires:863c7e73-8249-4780-a08e-0ff2bdddda86"
...
7) "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86"

  會話資訊存放在“spring:session:sessions:”開頭的Key中,863c7e73-8249-4780-a08e-0ff2bdddda86代表一個會話id,“spring:session:sessions”是一個Hash資料結構,可以用Redis HASH相關的命令來檢視這個使用者會話的資料,使用hgetall檢視會話所有的資訊:

>hgetall "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86"
1) "sessionAttr:user"
2) "maxInactiveInterval"
.......

  使用以下命令來檢視該Session的user資訊:

>HMGET "spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86"   sessionAttr:user

  sessionAttr:user是Spring Session存入Redis的Key值,sessionAttr:是其字首,user是我們在Spring Boot中設定會話的Key。其他Spring Boot預設建立的Key還有:

  creationTime,建立時間。

  maxInactiveInterval,指定過期時間(秒)。

  lastAccessedTime,上次訪問時間。

  sessionAttr,以“sessionAttr:”為字首的會話資訊,比如sessionAttr: user。

  因此,Spring Session使用Redis儲存的會話將採用如下的Redis操作,類似如下:

>HMSET spring:session:sessions:863c7e73-8249-4780-a08e-0ff2bdddda86 creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2

  注意:Spring Session的Redis實現並不是每次通過Session類獲取會話資訊或者儲存的時候都會呼叫Redis操作,它會先嚐試從內部的HashMap讀取值,如果沒有,才呼叫Redis的HMGET操作。同樣,當儲存會話的時候,也沒有立即呼叫Redis操作,而是先儲存到HashMap中,等待服務請求結束後再將變化的值使用HMSET更新。如果你想在儲存會話操作後立即更新到Redis中,需要配置成IMMEDIATE模式,修改配置屬性:

  spring.session.redis.flushMode=IMMEDIATE

  我們注意到,還有另外一個Redis Key是“spring:session:sessions:expires:863c7e73-8249-4780- a08e-0ff2bdddda86”,這是因為Redis會話過期並沒有直接使用在session:sessions:key變數上,而是專門用在session:sessions:expires:key上,當此Key過期後,會自動清除對應的會話資訊。使用ttl檢視會話過期時間:

>ttl spring:session:sessions:expires:863c7e73-8249-4780-a08e-0ff2bdddda86
(integer) 1469

  預設是1800秒,即30分鐘,現在只剩下1469秒。

3.3 Nginx+Redis

  在前文中,我們已經配置了:

upstream backend {   
server 127.0.0.1:9000;
server 127.0.0.1:9001
}

  假設在本機上部署了兩個Spring Boot應用,使用埠分別是9000和9001。進入工程目錄,執行mvn package,我們看到ch15.springsession\target\目錄下生成了ch17.springsession-0.0.1- SNAPSHOT.jar。然後進入命令列,進入target目錄,啟動這個Spring Boot應用:

java -jar target/ch15.springsession-0.0.1-SNAPSHOT.jar  --server.port=9000

開啟另外一個命令視窗,進入工程目錄,執行:

java -jar target/ch15.springsession-0.0.1-SNAPSHOT.jar  --server.port=9001

  這時候,我們就有兩臺Spring Boot應用。接下來,我們訪問以下地址,並重新整理多次:

http://127.0.0.1/putsession.html 

  這時候就看到兩個Spring Boot應用均有日誌輸出,比如9000埠的應用控制檯輸出如下:

class org.springframework.session.web.http.SessionRepositoryFilter....
863c7e73-8249-4780-a08e-0ff2bdddda86

  9001埠的Spring Boot應用也有類似輸出:

class org.springframework.session.web.http.SessionRepositoryFilter....
863c7e73-8249-4780-a08e-0ff2bdddda86

  我們看到,兩個Spring Boot應用都具有相同的sessionId,如果停掉任意一臺應用,系統還有另外一臺伺服器提供服務,會話資訊儲存在Redis中。

  以上內容節選自《Spring Boot2精髓:從構建小系統到架構分散式大系統》,點此連結可在博文視點官網檢視此書。
                  圖片描述
  想及時獲得更多精彩文章,可在微信中搜尋“博文視點”或者掃描下方二維碼並關注。
                    圖片描述

相關文章