Springboot3.0打造能落地的高并發(fā)仿12306售票系統(tǒng)吾愛(ài)青青草地
Springboot3介紹
Springboot3.0打造能落地的高并發(fā)仿12306售票系統(tǒng)
下栽の地止:https://lexuecode.com/6719.html
從 2018 年 2 月 28 號(hào)發(fā)布 Spring Boot 2.0 版本開(kāi)始,整個(gè) 2.X 版本已經(jīng)經(jīng)過(guò)了 4 年多的時(shí)間,累計(jì)發(fā)布了 95 個(gè)不同的版本,而就在前不久,2.X 系列的也已經(jīng)迎來(lái)了他的最終版本:2.7。
前幾天我還寫了一篇關(guān)于 Spring Boot 從 2.1 版本升級(jí)到 2.7 的文章,而現(xiàn)在,Spring Boot 3.0 也要來(lái)了!
時(shí)間就定在本月的 24 號(hào),Spring Boot 將要發(fā)布 3.0 的最終 RELEASE 版本。
截止到現(xiàn)在為止,Spring Boot3已經(jīng)發(fā)布了 6 個(gè)版本,累計(jì) 5 個(gè)里程碑版本,2 個(gè) RC 候選版,現(xiàn)在就跟著我一起看下 Spring Boot 3.0 將會(huì)要哪些重大的變化。
JAVA 17
對(duì)于我們比較關(guān)注的第一個(gè)最重要的事情就是,Spring Boot3 版本最低支持 Java17,還在萬(wàn)年 Java8 的同學(xué),該升級(jí)就升級(jí)了吧,這里介紹一下關(guān)于 Java17 之后的一些重要改變。
record
record 是在 Java14中引入的,openjdk.org/jeps/395 。
以前我們寫一個(gè)類需要寫一堆 get、set 方法,后來(lái)有了lombok之后這些都省了,現(xiàn)在 Java 給我們提供了原生的寫法。
public record User() {}
復(fù)制代碼
本質(zhì)上 record 修飾之后的類就是一個(gè) final 類,而且他的父類不是 Object,也不是余某軍,而是 java.lang.Record。
record 類屬性只能聲明在頭部,所有的成員變量都是 public final 的,而且只能聲明靜態(tài)屬性,但是可以聲明成員方法和靜態(tài)方法。
public record User(String username) {
static int id;
public String getName(){
return this.username;
}
}
復(fù)制代碼
text blocks
text blocks 文本塊是在 Java13 引入的,并且在 Java15 版本成為永久特性,openjdk.org/jeps/378。
以前我們復(fù)制一個(gè)多行的字符串到 Java 里,會(huì)自動(dòng)加上行終止符。
String sql = "SELECT\n" +
"\t* \n" +
"FROM\n" +
"\tsys_user0 \n" +
"WHERE\n" +
"\tuser_name = 'abc'";
復(fù)制代碼
而有了文本塊的功能之后,可以幫助我們更方便的定義包含多行文本的字符串字面量,他使用三引號(hào)作為開(kāi)始和結(jié)束的分隔符。
String sqlBlock = """
SELECT
*
FROM
sys_user0
WHERE
user_name = 'abc'
""";
復(fù)制代碼
switch表達(dá)式
switch表達(dá)式是在 Java12 中引入的,在 Java14 成為永久特性,openjdk.org/jeps/361。
升級(jí)后的 switch 其實(shí)包含兩個(gè)特性,一個(gè)是允許 case 使用多個(gè)常量,另外一個(gè)就是有返回值。
新增case x-> 語(yǔ)法,使用方面更加簡(jiǎn)潔,而且不需要再每個(gè) case 寫一個(gè) break了。
String name = "xiao";
int ret = switch (name) {
case "ai" -> 1;
case "xiao", "xian" -> 2;
default -> 0;
};
復(fù)制代碼
Pattern Matching 模式匹配
模式匹配可以幫助我們簡(jiǎn)化instanceof代碼。
if (obj instanceof String s) {
System.out.println(s.toLowerCase());
}
復(fù)制代碼
還可以在 switch-case 語(yǔ)句使用:
static double getDoubleUsingSwitch(Object o) {
return switch (o) {
case Integer i -> i.doubleValue();
case Float f -> f.doubleValue();
case String s -> Double.parseDouble(s);
default -> 0d;
};
}
復(fù)制代碼
sealed 密封類
sealed 在 Java15中引入,在 Java17成為永久特性。
sealed 密封類的主要作用就是限制類的繼承。
比如我們有 Animal類,Dog 和 Cat 分別繼承它,實(shí)現(xiàn)了 eat 方法,他們吃的動(dòng)作是不一樣的,但是我們不希望人能繼承 Animal,不允許他去繼承動(dòng)物吃的行為,就可以像下面這樣通過(guò) sealed 和 permits 關(guān)鍵字限制它是一個(gè)密封類,只有貓和狗能夠繼承它。
需要注意,父類被定義為 sealed 之后,子類必須是 sealed、 non-sealed 或者 final。
public abstract sealed class Animal permits Cat, Dog {
public abstract void eat();
}
public non-sealed class Dog extends Animal{
@Override
public void eat() {
System.out.println("dog eat");
}
}
public non-sealed class Cat extends Animal{
@Override
public void eat() {
System.out.println("cat eat");
}
}
復(fù)制代碼
Jakarta EE 9
另外一個(gè)很重要的變化就是本次升級(jí)之后,最低只支持 Jakarta EE 9,使用 Servlet5.0 和 JPA3.0 規(guī)范,不過(guò)最新版本RC2已經(jīng)升級(jí)到了 JakartaEE 10,默認(rèn)使用 Servlet6.0 和 JPA3.1 規(guī)范。
有些同學(xué)可能連 Jakarta 是什么都不知道,這個(gè)英文單詞是印尼首都雅加達(dá)的意思,其實(shí)就是我們知道的 JavaEE 改名之后就叫 JakartaEE,比如我們之前的javax.servlet包現(xiàn)在就叫jakarta.servlet。
也因此,代碼中所有使用到比如 HttpServletRequest 對(duì)象的 import 都需要修改。
import javax.servlet.http.HttpServletRequest;
改為
import jakarta.servlet.http.HttpServletRequest;
復(fù)制代碼
Spring Native
Spring Native 也是升級(jí)的一個(gè)重大特性,支持使用 GraalVM 將 Spring 的應(yīng)用程序編譯成本地可執(zhí)行的鏡像文件,可以顯著提升啟動(dòng)速度、峰值性能以及減少內(nèi)存使用。
我們傳統(tǒng)的應(yīng)用都是編譯成字節(jié)碼,然后通過(guò) JVM 解釋并最終編譯成機(jī)器碼來(lái)運(yùn)行,而 Spring Native 則是通過(guò) AOT 提前編譯為機(jī)器碼,在運(yùn)行時(shí)直接靜態(tài)編譯成可執(zhí)行文件,不依賴 JVM。
關(guān)于 AOT 技術(shù),在我之前寫過(guò)的文章中有提及到:這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起!。
演示
這里我簡(jiǎn)單演示一下怎么使用,首先我們需要做一些準(zhǔn)備工作:
1. www.graalvm.org/downloads/ 下載GraalVM,指定JAVA_HOME,export JAVA_HOME=/Users/user/Desktop/graalvm-ce-java17-22.3.0/Contents/Home
2. 下載個(gè)新一點(diǎn)的 IDEA,比如我使用的是最新的 EAP 版本
3. 下載個(gè)新一點(diǎn)的 JDK,比如使用 JDK17
然后通過(guò) Spring Initialzr 創(chuàng)建一個(gè)新項(xiàng)目,使用最新版本Spring Boot 3.0.0-SNAPSHOT,勾選GraalVM Native Support,創(chuàng)建好項(xiàng)目之后添加一個(gè)測(cè)試的Controller。
@RestController
public class TestController {
@GetMapping("/")
public String hello(){
return "GraalVM ...";
}
}
復(fù)制代碼
然后直接運(yùn)行程序,發(fā)現(xiàn)啟動(dòng)時(shí)間花費(fèi)了大概 1 秒。
然后執(zhí)行命令,生成鏡像文件:
./gradlew nativeCompile
復(fù)制代碼
這個(gè)過(guò)程挺耗時(shí)的,花了大概 2 分多鐘才生成好。
最后執(zhí)行命令:
./build/native/nativeCompile/demo2
復(fù)制代碼
我們看到,最終啟動(dòng)時(shí)間是 0.082 秒,快了 10 多倍。
這里我使用的是 gradle,如果使用 maven 的話,使用如下的命令:
1. mvnw -Pnative native:compile
2. ./target/demo2
復(fù)制代碼
其他依賴升級(jí)
Spring Boot 3 最低依賴 Spring6 版本,因此對(duì)應(yīng)的 Spring 版本也該換了(不會(huì)有人還在用 Spring2 的吧),其他的依賴升級(jí)如下:
? Kotlin 1.7+
? Lombok 1.18.22+ (JDK17 support支持版本)
? Gradle 7.3+
另外我想說(shuō)的是,SpringBoot2.7引入了新的自動(dòng)裝配方式META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,原來(lái)的寫法spring.factories在 3.0 版本以下還能兼容,3.0 新版本之后,老的寫法spring.factories不能使用了,中間件相關(guān)的開(kāi)發(fā)同學(xué)要注意了。
其他一些關(guān)于配置的變化,Spring MVC 的一些小變化就不說(shuō)了,更新日志到時(shí)候都可以看到。
最后,如果想升級(jí)的話,在新版本發(fā)布之后,會(huì)有一個(gè)基于Spring Boot 2.7 版本的遷移指南
Springboot3.0打高并發(fā)仿12306系統(tǒng) - 大型分布式架構(gòu)設(shè)計(jì)
一般網(wǎng)站,剛開(kāi)始的做法,是三臺(tái)服務(wù)器,一臺(tái)部署應(yīng)用,一臺(tái)部署數(shù)據(jù)庫(kù),一臺(tái)部署NFS文件系統(tǒng)。
這是前幾年比較傳統(tǒng)的做法,之前見(jiàn)到一個(gè)網(wǎng)站10萬(wàn)多會(huì)員,垂直服裝設(shè)計(jì)門戶,N多圖片。使用了一臺(tái)服務(wù)器部署了應(yīng)用,數(shù)據(jù)庫(kù)以及圖片存儲(chǔ)。出現(xiàn)了很多性能問(wèn)題。
如下圖:
但是,目前主流的網(wǎng)站架構(gòu)已經(jīng)發(fā)生了翻天覆地的變化。一般都會(huì)采用集群的方式,進(jìn)行高可用設(shè)計(jì)。至少是下面這個(gè)樣子:
使用集群對(duì)應(yīng)用服務(wù)器進(jìn)行冗余,實(shí)現(xiàn)高可用;(負(fù)載均衡設(shè)備可與應(yīng)用一塊部署)
使用數(shù)據(jù)庫(kù)主備模式,實(shí)現(xiàn)數(shù)據(jù)備份和高可用;
4、系統(tǒng)容量預(yù)估
預(yù)估步驟:
注冊(cè)用戶數(shù)-日均UV量-每日的PV量-每天的并發(fā)量;
峰值預(yù)估:平常量的2~3倍;
根據(jù)并發(fā)量(并發(fā),事務(wù)數(shù)),存儲(chǔ)容量計(jì)算系統(tǒng)容量。
根據(jù)客戶需求:3~5年用戶數(shù)達(dá)到1000萬(wàn)注冊(cè)用戶,可以做每秒并發(fā)數(shù)預(yù)估:
每天的UV為200萬(wàn)(二八原則);
每日每天點(diǎn)擊瀏覽30次;
PV量:200*30=6000萬(wàn);
集中訪問(wèn)量:24*0.2=4.8小時(shí)會(huì)有6000萬(wàn)*0.8=4800萬(wàn)(二八原則);
每分并發(fā)量:4.8*60=288分鐘,每分鐘訪問(wèn)4800/288=16.7萬(wàn)(約等于);
每秒并發(fā)量:16.7萬(wàn)/60=2780(約等于);
假設(shè):高峰期為平常值的三倍,則每秒的并發(fā)數(shù)可以達(dá)到8340次。
1毫秒=1.3次訪問(wèn);
沒(méi)好好學(xué)數(shù)學(xué)后悔了吧??。ú恢酪陨纤闶欠裼绣e(cuò)誤,呵呵~~)
服務(wù)器預(yù)估:(以tomcat服務(wù)器舉例)
按一臺(tái)web服務(wù)器,支持每秒300個(gè)并發(fā)計(jì)算。平常需要10臺(tái)服務(wù)器(約等于);[tomcat默認(rèn)配置是150],高峰期需要30臺(tái)服務(wù)器;
容量預(yù)估:70/90原則
系統(tǒng)CPU一般維持在70%左右的水平,高峰期達(dá)到90%的水平,是不浪費(fèi)資源,并比較穩(wěn)定的。內(nèi)存,IO類似。
以上預(yù)估僅供參考,因?yàn)榉?wù)器配置,業(yè)務(wù)邏輯復(fù)雜度等都有影響。在此CPU,硬盤,網(wǎng)絡(luò)等不再進(jìn)行評(píng)估。
5、網(wǎng)站架構(gòu)分析
根據(jù)以上預(yù)估,有幾個(gè)問(wèn)題:
需要部署大量的服務(wù)器,高峰期計(jì)算,可能要部署30臺(tái)Web服務(wù)器。并且這三十臺(tái)服務(wù)器,只有秒殺,活動(dòng)時(shí)才會(huì)用到,存在大量的浪費(fèi)。
所有的應(yīng)用部署在同一臺(tái)服務(wù)器,應(yīng)用之間耦合嚴(yán)重。需要進(jìn)行垂直切分和水平切分。
大量應(yīng)用存在冗余代碼
服務(wù)器Session同步耗費(fèi)大量?jī)?nèi)存和網(wǎng)絡(luò)帶寬
數(shù)據(jù)需要頻繁訪問(wèn)數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)訪問(wèn)壓力巨大。
大型網(wǎng)站一般需要做以下架構(gòu)優(yōu)化(優(yōu)化是架構(gòu)設(shè)計(jì)時(shí),就要考慮的,一般從架構(gòu)/代碼級(jí)別解決,調(diào)優(yōu)主要是簡(jiǎn)單參數(shù)的調(diào)整,比如JVM調(diào)優(yōu);如果調(diào)優(yōu)涉及大量代碼改造,就不是調(diào)優(yōu)了,屬于重構(gòu)):
業(yè)務(wù)拆分
應(yīng)用集群部署(分布式部署,集群部署和負(fù)載均衡)
多級(jí)緩存
單點(diǎn)登錄(分布式Session)
數(shù)據(jù)庫(kù)集群(讀寫分離,分庫(kù)分表)
服務(wù)化
消息隊(duì)列
其他技術(shù)
6、網(wǎng)站架構(gòu)優(yōu)化
(1)業(yè)務(wù)拆分
根據(jù)業(yè)務(wù)屬性進(jìn)行垂直切分,劃分為產(chǎn)品子系統(tǒng),購(gòu)物子系統(tǒng),支付子系統(tǒng),評(píng)論子系統(tǒng),客服子系統(tǒng),接口子系統(tǒng)(對(duì)接如進(jìn)銷存,短信等外部系統(tǒng))。
根據(jù)業(yè)務(wù)子系統(tǒng)進(jìn)行等級(jí)定義,可分為核心系統(tǒng)和非核心系統(tǒng)。核心系統(tǒng):產(chǎn)品子系統(tǒng),購(gòu)物子系統(tǒng),支付子系統(tǒng);非核心:評(píng)論子系統(tǒng),客服子系統(tǒng),接口子系統(tǒng)。
業(yè)務(wù)拆分作用:提升為子系統(tǒng)可由專門的團(tuán)隊(duì)和部門負(fù)責(zé),專業(yè)的人做專業(yè)的事,解決模塊之間耦合以及擴(kuò)展性問(wèn)題;每個(gè)子系統(tǒng)單獨(dú)部署,避免集中部署導(dǎo)致一個(gè)應(yīng)用掛了,全部應(yīng)用不可用的問(wèn)題。
等級(jí)定義作用:用于流量突發(fā)時(shí),對(duì)關(guān)鍵應(yīng)用進(jìn)行保護(hù),實(shí)現(xiàn)優(yōu)雅降級(jí);保護(hù)關(guān)鍵應(yīng)用不受到影響。
拆分后的架構(gòu)圖:
參考部署方案2
如上圖每個(gè)應(yīng)用單獨(dú)部署,核心系統(tǒng)和非核心系統(tǒng)組合部署
(2)應(yīng)用集群部署(分布式,集群,負(fù)載均衡)
分布式部署:將業(yè)務(wù)拆分后的應(yīng)用單獨(dú)部署,應(yīng)用直接通過(guò)RPC進(jìn)行遠(yuǎn)程通信;
集群部署:電商網(wǎng)站的高可用要求,每個(gè)應(yīng)用至少部署兩臺(tái)服務(wù)器進(jìn)行集群部署;
負(fù)載均衡:是高可用系統(tǒng)必須的,一般應(yīng)用通過(guò)負(fù)載均衡實(shí)現(xiàn)高可用,分布式服務(wù)通過(guò)內(nèi)置的負(fù)載均衡實(shí)現(xiàn)高可用,關(guān)系型數(shù)據(jù)庫(kù)通過(guò)主備方式實(shí)現(xiàn)高可用。
集群部署后架構(gòu)圖:
(3)多級(jí)緩存
緩存按照存放的位置一般可分為兩類本地緩存和分布式緩存。本案例采用二級(jí)緩存的方式,進(jìn)行緩存的設(shè)計(jì)。一級(jí)緩存為本地緩存,二級(jí)緩存為分布式緩存。(還有頁(yè)面緩存,片段緩存等,那是更細(xì)粒度的劃分)
一級(jí)緩存,緩存數(shù)據(jù)字典,和常用熱點(diǎn)數(shù)據(jù)等基本不可變/有規(guī)則變化的信息,二級(jí)緩存緩存需要的所有緩存。當(dāng)一級(jí)緩存過(guò)期或不可用時(shí),訪問(wèn)二級(jí)緩存的數(shù)據(jù)。如果二級(jí)緩存也沒(méi)有,則訪問(wèn)數(shù)據(jù)庫(kù)。
緩存的比例,一般1:4,即可考慮使用緩存。(理論上是1:2即可)。
根據(jù)業(yè)務(wù)特性可使用以下緩存過(guò)期策略:
緩存自動(dòng)過(guò)期;
緩存觸發(fā)過(guò)期;
(4)單點(diǎn)登錄(分布式Session)
系統(tǒng)分割為多個(gè)子系統(tǒng),獨(dú)立部署后,不可避免的會(huì)遇到會(huì)話管理的問(wèn)題。一般可采用Session同步,Cookies,分布式Session方式。電商網(wǎng)站一般采用分布式Session實(shí)現(xiàn)。
再進(jìn)一步可以根據(jù)分布式Session,建立完善的單點(diǎn)登錄或賬戶管理系統(tǒng)。
流程說(shuō)明
用戶第一次登錄時(shí),將會(huì)話信息(用戶Id和用戶信息),比如以用戶Id為Key,寫入分布式Session;
用戶再次登錄時(shí),獲取分布式Session,是否有會(huì)話信息,如果沒(méi)有則調(diào)到登錄頁(yè);
一般采用Cache中間件實(shí)現(xiàn),建議使用Redis,因此它有持久化功能,方便分布式Session宕機(jī)后,可以從持久化存儲(chǔ)中加載會(huì)話信息;
存入會(huì)話時(shí),可以設(shè)置會(huì)話保持的時(shí)間,比如15分鐘,超過(guò)后自動(dòng)超時(shí);
結(jié)合Cache中間件,實(shí)現(xiàn)的分布式Session,可以很好的模擬Session會(huì)話。
(5)數(shù)據(jù)庫(kù)集群(讀寫分離,分庫(kù)分表)
大型網(wǎng)站需要存儲(chǔ)海量的數(shù)據(jù),為達(dá)到海量數(shù)據(jù)存儲(chǔ),高可用,高性能一般采用冗余的方式進(jìn)行系統(tǒng)設(shè)計(jì)。一般有兩種方式讀寫分離和分庫(kù)分表。
讀寫分離:一般解決讀比例遠(yuǎn)大于寫比例的場(chǎng)景,可采用一主一備,一主多備或多主多備方式。
本案例在業(yè)務(wù)拆分的基礎(chǔ)上,結(jié)合分庫(kù)分表和讀寫分離。如下圖: