使用Spring Session實現Spring Boot水平擴充套件
本文使用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中。