面試總結(jié):秒殺設(shè)計(jì)、AQS 、synchronized相關(guān)問題
1、面試官:如何設(shè)計(jì)一個(gè)秒殺系統(tǒng)?請(qǐng)你闡述流程?
這是一篇參考的文章:如何設(shè)計(jì)一個(gè)秒殺系統(tǒng)
秒殺系統(tǒng)要解決的幾個(gè)問題?
① 高并發(fā)
秒殺的特點(diǎn)是時(shí)間極短、 瞬間用戶量大。在秒殺活動(dòng)持續(xù)時(shí)間內(nèi),Redis 服務(wù)器需要承受大量的用戶請(qǐng)求,在大量請(qǐng)求條件下,緩存雪崩,緩存擊穿,緩存穿透這些問題都是有可能發(fā)生的。
一旦緩存失效,或者緩存無效,每秒上萬甚至十幾萬的QPS(每秒請(qǐng)求數(shù))直接打到數(shù)據(jù)庫,基本上都要把庫打掛掉,而且你的服務(wù)不單單是做秒殺的還涉及其他的業(yè)務(wù),你沒做降級(jí)、限流、熔斷啥的,別的一起掛,小公司的話可能全站崩潰404。
為此,在設(shè)計(jì)秒殺系統(tǒng)時(shí),首先要考慮并發(fā)安全和用戶訪問效率,二者缺一不可!
② 超賣
但凡設(shè)計(jì)到商品購買,秒殺購物問題,最需要注意的就是超賣問題,因?yàn)橐坏┯捎诔绦虿话踩瑢?dǎo)致超賣問題產(chǎn)生,不光需要賠付商家損失,還需要追究秒殺系統(tǒng)的開發(fā)者的責(zé)任!
③ 惡意請(qǐng)求
在秒殺購物時(shí),商品價(jià)格比較低,價(jià)值較高的商品可能會(huì)被一些惡意的第三方,開多臺(tái)機(jī)器執(zhí)行搶購腳本,機(jī)器搶購肯定要比我們?nèi)耸謩?dòng)點(diǎn)擊要快,所以,在設(shè)計(jì)秒殺系統(tǒng)時(shí),要防止 不良程序員 的惡意搶購。
④ 鏈接暴露
假如設(shè)置定時(shí)秒殺開啟,在未到秒殺開啟時(shí)間之前,下單購買按鈕應(yīng)該是禁用的(不可點(diǎn)擊),但是,如果我們請(qǐng)求下單的鏈接沒有經(jīng)過網(wǎng)關(guān)加密封裝,而是直接以原鏈接的方式依附于下單購買按鈕,那么 F12 時(shí),就可以獲取下單購買鏈接 URL,然后直接去請(qǐng)求下單鏈接,跳過點(diǎn)擊方式直接購買商品!
如何解決上面遇到的幾個(gè)問題?
① 秒殺模塊微服務(wù)化
對(duì)于秒殺搶購系統(tǒng),將其設(shè)計(jì)成單獨(dú)一個(gè)模塊,單獨(dú)部署一臺(tái)或者多太服務(wù)器,這樣可以避免服務(wù)崩潰時(shí),公司其他項(xiàng)目可以正常運(yùn)行不受影響。
與此同時(shí),要單獨(dú)給秒殺系統(tǒng)建立一個(gè)數(shù)據(jù)庫,現(xiàn)在的互聯(lián)網(wǎng)架構(gòu)部署都是分庫的,一樣的就是訂單服務(wù)對(duì)應(yīng)訂單庫,秒殺我們也給他建立自己的秒殺數(shù)據(jù)庫,防止服務(wù)崩潰對(duì)其他數(shù)據(jù)庫造成影響。
② 秒殺鏈接加鹽
URL動(dòng)態(tài)化,通過 MD5 之類的加密算法加密隨機(jī)的字符串去做 URL,然后通過前端代碼獲取 URL 后臺(tái)校驗(yàn)后才能通過。
③ Redis集群
如果在大請(qǐng)求量下,單機(jī)的Redis頂不住,那就多找?guī)讉€(gè)兄弟,秒殺本來就是讀多寫少,通過 Redis 集群,主從同步、讀寫分離,再加上 哨兵、開啟 持久化,來保證 Redis 服務(wù)高可用!
④ 通過 Nginx 做負(fù)載均衡
Nginx 是 高性能的web服務(wù)器,并發(fā)隨便頂幾萬不是夢(mèng),但是我們的 Tomcat 只能頂幾百的并發(fā),我們可以通過Nginx 負(fù)載均衡,平分大并發(fā)量的請(qǐng)求給多臺(tái)服務(wù)器的 Tomcat,在秒殺開啟的時(shí)候可以多租點(diǎn)流量機(jī)。
⑤ 秒殺頁面資源靜態(tài)化
秒殺一般都是特定的商品還有頁面模板,現(xiàn)在一般都是前后端分離的,所以頁面一般都是不會(huì)經(jīng)過后端的,但是前端也要自己的服務(wù)器啊,那就把能提前放入**cdn服務(wù)器 **的東西都放進(jìn)去,反正把所有能提升效率的步驟都做一下,減少真正秒殺時(shí)候服務(wù)器的壓力。
⑥ 下單按鈕控制
在沒到秒殺開始時(shí)間之前,一般下單按鈕都是置灰的,只有時(shí)間到了,才能點(diǎn)擊。這是因?yàn)榕麓蠹以跁r(shí)間快到的最后幾秒秒瘋狂請(qǐng)求服務(wù)器,然后還沒到秒殺的時(shí)候基本上服務(wù)器就掛了。
這個(gè)時(shí)候就需要前端的配合,定時(shí)去請(qǐng)求你的后端服務(wù)器,獲取最新的北京時(shí)間,到時(shí)間點(diǎn)再給按鈕可用狀態(tài)。按鈕可以點(diǎn)擊之后也得給他置灰?guī)酌耄蝗凰粯釉陂_始之后一直點(diǎn)的。
⑦ 前后端限流
限流可以分為 前端限流 和 后端限流。
前端限流:這個(gè)很簡(jiǎn)單,一般秒殺搶購,下單按鈕不會(huì)讓你一直點(diǎn)的,一般都是點(diǎn)擊一下或者兩下然后幾秒之后才可以繼續(xù)點(diǎn)擊,這也是保護(hù)服務(wù)器的一種手段。
后端限流:秒殺的時(shí)候肯定是涉及到后續(xù)的訂單生成和支付等操作,但是都只是成功的幸運(yùn)兒才會(huì)走到那一步,那一旦 100 個(gè)產(chǎn)品賣光了,return
了一個(gè) false
,前端直接秒殺結(jié)束,然后你后端也關(guān)閉后續(xù)無效請(qǐng)求的介入了。
⑧ 庫存預(yù)熱
秒殺的本質(zhì),就是對(duì)庫存的搶奪。如果每個(gè)秒殺下單的用戶請(qǐng)求過來,都去數(shù)據(jù)庫查詢庫存校驗(yàn)庫存,然后扣減庫存,這樣不光效率低下,而且數(shù)據(jù)庫壓力也是巨大的!
既然數(shù)據(jù)庫頂不住,但是他的兄弟非關(guān)系型的數(shù)據(jù)庫 Redis 能頂??!
超賣問題:
我們要在開始秒殺之前,通過定時(shí)任務(wù)提前把商品的庫存加載到 Redis 中去,讓整個(gè)庫存校驗(yàn)流程都在 Redis 里面去做,然后等到秒殺活動(dòng)結(jié)束了,再異步的去修改數(shù)據(jù)庫中庫存就好了。
但是用了 Redis 就有一個(gè)問題了,我們上面說了我們采用 主從 Redis,就是我們會(huì)先去讀取庫存,再判斷庫存,當(dāng)有庫存時(shí)才會(huì)去減庫存,正常情況沒問題,但是高并發(fā)的情況問題就很大了。
就比如現(xiàn)在庫存只剩下 1 個(gè)了,我們高并發(fā)嘛,4 個(gè)服務(wù)器一起查詢了發(fā)現(xiàn)都是還有 1 個(gè),那大家都覺得是自己搶到了,就都去扣庫存,那結(jié)果就變成了 -3,這種情況下,只有一個(gè)請(qǐng)求是真的搶到商品了,其他 3 個(gè)都是超賣的。
如何解決?
可以通過使用 Lua 腳本來解決超賣問題。
**Lua 腳本是類似Redis事務(wù),有一定的原子性,不會(huì)被其他命令插隊(duì),可以完成一些 Redis 事務(wù)性的操作。**這點(diǎn)是關(guān)鍵!
把判斷庫存、扣減庫存的操作都寫在一個(gè) Lua 腳本中,并將該腳本交給 Redis 去執(zhí)行,當(dāng) Redis 中庫存數(shù)量減到 0 之后,后面扣庫存的請(qǐng)求都直接 return false
。
⑨ 限流&降級(jí)&熔斷&隔離
不怕一萬就怕萬一,萬一秒殺系統(tǒng)真的頂不住了,限流,頂不住就擋一部分出去。但是不能說不行,降級(jí),降級(jí)了還是被打掛了,熔斷,至少不要影響別的系統(tǒng),隔離,你本身就獨(dú)立的,但是你會(huì)調(diào)用其他的系統(tǒng)嘛,你快不行了你別拖累兄弟們啊。
2、面試官:AQS源碼有了解過嗎?請(qǐng)你說一下加鎖和釋放鎖的流程
這一面試題答案參考自三太子敖丙的文章:深入淺出學(xué)習(xí)AQS組件
① AQS紹
AQS
中 維護(hù) 基本介了一個(gè)volatile int state
(代表共享資源)和一個(gè) FIFO
線程等待隊(duì)列(多線程爭(zhēng)用資源被阻塞時(shí)會(huì)進(jìn)入此隊(duì)列)。
這里volatile
能夠保證多線程下的可見性,當(dāng)state = 1
則代表當(dāng)前對(duì)象鎖已經(jīng)被占有,其他線程來加鎖時(shí)則會(huì)失敗,加鎖失敗的線程會(huì)被放入一個(gè) FIFO
的等待隊(duì)列中,并會(huì)被 UNSAFE.park()
操作掛起,等待其他獲取鎖的線程釋放鎖才能夠被喚醒。
另外state
的操作都是通過CAS
來保證其并發(fā)修改的安全性。
如圖所示:
AQS
中提供了很多關(guān)于鎖的實(shí)現(xiàn)方法:
getState():
獲取鎖的標(biāo)志 state 值。
setState():
設(shè)置鎖的標(biāo)志 state 值。
tryAcquire(int):
獨(dú)占方式獲取鎖。嘗試獲取資源,成功則返回 true,失敗則返回 false。
tryRelease(int):
獨(dú)占方式釋放鎖。嘗試釋放資源,成功則返回 true,失敗則返回 false。
② 加鎖與競(jìng)爭(zhēng)鎖使用場(chǎng)景分析
如果同時(shí)有三個(gè)線程并發(fā)搶占鎖,此時(shí)線程一搶占鎖成功,線程二和線程三搶占鎖失敗,具體執(zhí)行流程如下:
此時(shí)AQS
內(nèi)部數(shù)據(jù)為:
具體看下?lián)屨兼i代碼實(shí)現(xiàn):java.util.concurrent.locks.ReentrantLock .NonfairSync
static final class NonfairSync extends Sync { // 加鎖 final void lock() { // CAS 修改 state 的值為 1 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } // 嘗試競(jìng)爭(zhēng)資源 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
這里使用的 ReentrantLock 非公平鎖,線程進(jìn)來直接利用CAS
嘗試搶占鎖,如果搶占成功state
值回被改為 1,且設(shè)置獨(dú)占鎖線程對(duì)象為當(dāng)前線程。
// CAS 嘗試搶占鎖 protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } // 設(shè)置獨(dú)占鎖線程對(duì)象為當(dāng)前線程 protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }
線程一搶占鎖成功后,state
變?yōu)?1,線程二通過CAS
修改state
變量必然會(huì)失敗。此時(shí)AQS
中FIFO
(First In First Out 先進(jìn)先出)隊(duì)列中。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire()
方法的具體實(shí)現(xiàn)是通過內(nèi)部調(diào)用nonfairTryAcquire()
方法,這個(gè)方法執(zhí)行的邏輯如下:
首先會(huì)獲取state
的值,如果不為0則說明當(dāng)前對(duì)象的鎖已經(jīng)被其他線程所占有。
接著判斷占有鎖的線程是否為當(dāng)前線程,如果是則累加state
值,這就是可重入鎖的具體實(shí)現(xiàn),累加state
值,釋放鎖的時(shí)候也要依次遞減state
值。
如果state
為 0,則執(zhí)行CAS
操作,嘗試更新state
值為 1,如果更新成功則代表當(dāng)前線程加鎖成功。
③ 釋放鎖使用場(chǎng)景分析
釋放鎖的過程,首先是線程一釋放鎖,釋放鎖后會(huì)喚醒head
節(jié)點(diǎn)的后置節(jié)點(diǎn),也就是我們現(xiàn)在的線程二,具體操作流程如下:
執(zhí)行完后等待隊(duì)列數(shù)據(jù)如下:
此時(shí)線程二已經(jīng)被喚醒,繼續(xù)嘗試獲取鎖,如果獲取鎖失敗,則會(huì)繼續(xù)被掛起。
線程釋放鎖的代碼:
java.util.concurrent.locks.AbstractQueuedSynchronizer.release():
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
這里首先會(huì)執(zhí)行tryRelease()
方法,這個(gè)方法具體實(shí)現(xiàn)在ReentrantLock
中,如果tryRelease()
執(zhí)行成功,則繼續(xù)判斷 head
節(jié)點(diǎn)的 waitStatus
是否為 0,前面我們已經(jīng)看到過,head
的waitStatue為SIGNAL(-1),
這里就會(huì)執(zhí)行 unparkSuccessor()
方法來喚醒 head 的后置節(jié)點(diǎn),也就是上面圖中線程二對(duì)應(yīng)的Node
節(jié)點(diǎn)。
ReentrantLock.tryRelease():
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
執(zhí)行完ReentrantLock.tryRelease()
后,state
被設(shè)置成 0,Lock 對(duì)象的獨(dú)占鎖被設(shè)置為 null。
接著執(zhí)行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()
方法,喚醒head
的后置節(jié)點(diǎn):
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
這里主要是將head
節(jié)點(diǎn)的waitStatus
設(shè)置為 0,然后解除head
節(jié)點(diǎn)next
的指向,使head
節(jié)點(diǎn)空置,等待著被垃圾回收。
此時(shí)重新將head
指針指向線程二對(duì)應(yīng)的Node
節(jié)點(diǎn),且使用LockSupport.unpark
方法來喚醒線程二。
被喚醒的線程二會(huì)接著嘗試獲取鎖,用CAS
指令修改state
數(shù)據(jù)。
3、面試官:請(qǐng)你談一談synchronized的實(shí)現(xiàn)原理
這一面試題答案參考文章:死磕 java同步系列之synchronized解析
synchronized 加鎖解鎖原理分析
sychronized 鎖,在Java內(nèi)存模型層面,涉及到 2 個(gè)指令(JMM 定義了8 個(gè)操作來完成主內(nèi)存和工作內(nèi)存的交互操作,參考文章:搜集了這么多資料,不信你還理解不了 JMM 內(nèi)存模型、volatile 關(guān)鍵字保證有序性和可見性實(shí)現(xiàn)原理?。?,lock
和 unlock
:
lock
,鎖定,作用于主內(nèi)存的變量,它把主內(nèi)存中的變量標(biāo)識(shí)為線程獨(dú)占狀態(tài)。unlock
,解鎖,作用于主內(nèi)存的變量,它把鎖定的變量釋放出來,釋放出來的變量才可以被其它線程鎖定。
這兩個(gè)指令并沒有直接提供給用戶使用,而是提供了兩個(gè)更高層次的指令 monitorenter
和 monitorexit
來隱式地使用 lock
和 unlock
指令。而 synchronized 就是使用 monitorenter
和 monitorexit
這兩個(gè)指令來實(shí)現(xiàn)的。
根據(jù)JVM規(guī)范的要求,在執(zhí)行 monitorenter
指令的時(shí)候,首先要去嘗試獲取對(duì)象的鎖,如果這個(gè)對(duì)象沒有被鎖定,或者當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,就把鎖的計(jì)數(shù)器加1,相應(yīng)地,在執(zhí)行 monitorexit
的時(shí)候會(huì)把計(jì)數(shù)器減 1,當(dāng)計(jì)數(shù)器減小為 0 時(shí),鎖就釋放了。
sychronized 鎖是如何保證,原子性、可見性、和一致性呢?
還是回到Java內(nèi)存模型上來,synchronized關(guān)鍵字底層是通過 monitorenter
和 monitorexit
實(shí)現(xiàn)的,而這兩個(gè)指令又是通過 lock
和 unlock
來實(shí)現(xiàn)的。
而 lock
和 unlock
在Java內(nèi)存模型中是必須滿足下面四條規(guī)則的:
- ① 一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行
lock
操作,但lock
操作可以被同一個(gè)線程執(zhí)行多次,多次執(zhí)行lock
后,只有執(zhí)行相同次數(shù)的unlock
操作,變量才能被解鎖。 - ② 如果對(duì)一個(gè)變量執(zhí)行
lock
操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行load
或assign
操作初始化變量的值; - ③ 如果一個(gè)變量沒有被
lock
操作鎖定,則不允許對(duì)其執(zhí)行unlock
操作,也不允許unlock
一個(gè)其它線程鎖定的變量; - ④ 對(duì)一個(gè)變量執(zhí)行
unlock
操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store
和write
操作;
通過規(guī)則 ①,我們知道對(duì)于 lock
和 unlock
之間的代碼,同一時(shí)刻只允許一個(gè)線程訪問,所以,synchronized 是具有原子性的。
通過規(guī)則 ① ② 和 ④,我們知道每次 lock
和 unlock
時(shí)都會(huì)從主內(nèi)存加載變量或把變量刷新回主內(nèi)存,而 lock 和 unlock 之間的變量(這里是指鎖定的變量)是不會(huì)被其它線程修改的,所以,synchronized 是具有可見性的。
通過規(guī)則 ① 和 ③ ,我們知道所有對(duì)變量的加鎖都要排隊(duì)進(jìn)行,且其它線程不允許解鎖當(dāng)前線程鎖定的對(duì)象,所以,synchronized 是具有有序性的。
綜上所述,synchronized 是可以保證原子性、可見性和有序性的。
synchronized 鎖優(yōu)化
Java在不斷進(jìn)化,同樣地,Java 中像 synchronized 這種關(guān)鍵字也在不斷優(yōu)化,synchronized 有如下三種狀態(tài):
- 偏向鎖,是指一段同步代碼一直被一個(gè)線程訪問,那么這個(gè)線程會(huì)自動(dòng)獲取鎖,降低獲取鎖的代價(jià)。
- 輕量級(jí)鎖,是指當(dāng)鎖是偏向鎖時(shí),被另一個(gè)線程所訪問,偏向鎖會(huì)升級(jí)為輕量級(jí)鎖,這個(gè)線程會(huì)通過自旋的方式嘗試獲取鎖,不會(huì)阻塞,提高性能。
- 重量級(jí)鎖,是指當(dāng)鎖是輕量級(jí)鎖時(shí),當(dāng)自旋的線程自旋了一定的次數(shù)后,還沒有獲取到鎖,就會(huì)進(jìn)入阻塞狀態(tài),該鎖升級(jí)為重量級(jí)鎖,重量級(jí)鎖會(huì)使其他線程阻塞,性能降低。
總結(jié)
(1)synchronized 在編譯時(shí)會(huì)在同步塊前后生成 monitorenter
和 monitorexit
字節(jié)碼指令;
(2)monitorenter
和 monitorexit
字節(jié)碼指令需要一個(gè)引用類型的參數(shù),基本類型不可以哦;
(3)monitorenter
和 monitorexit
字節(jié)碼指令更底層是使用Java內(nèi)存模型的 lock 和 unlock 指令;
(4)synchronized 是可重入鎖;
(5)synchronized 是非公平鎖;
(6)synchronized 可以同時(shí)保證原子性、可見性、有序性;
(7)synchronized 有三種狀態(tài):偏向鎖、輕量級(jí)鎖、重量級(jí)鎖;
本篇文章就到這里了,希望能給你帶來幫助,也希望你能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Redis Lettuce連接redis集群實(shí)現(xiàn)過程詳細(xì)講解
這篇文章主要介紹了Redis Lettuce連接redis集群實(shí)現(xiàn)過程,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01SpringBoot?項(xiàng)目打成?jar后加載外部配置文件的操作方法
這篇文章主要介紹了SpringBoot?項(xiàng)目打成?jar后加載外部配置文件的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03idea啟動(dòng)springmvc項(xiàng)目時(shí)報(bào)找不到類的解決方法
這篇文章主要介紹了idea啟動(dòng)springmvc項(xiàng)目時(shí)報(bào)找不到類的解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09解決FeignClient發(fā)送post請(qǐng)求異常的問題
這篇文章主要介紹了FeignClient發(fā)送post請(qǐng)求異常的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07Java實(shí)現(xiàn)的AES256加密解密功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)的AES256加密解密功能,結(jié)合完整實(shí)例形式分析了Java實(shí)現(xiàn)AES256加密解密功能的步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-02-02被遺忘的Java關(guān)鍵字transient的使用詳解
在 Java 中,transient 是一個(gè)關(guān)鍵字,用于指定一個(gè)類的字段(成員變量)在序列化時(shí)應(yīng)該被忽略。本文將通過示例為大家簡(jiǎn)單講講transient的使用,需要的可以參考一下2023-04-04在Android系統(tǒng)中使用WebViewClient處理跳轉(zhuǎn)URL的方法
這篇文章主要介紹了在Android系統(tǒng)中使用WebViewClient處理跳轉(zhuǎn)URL的方法,實(shí)現(xiàn)代碼為Java語言編寫,是需要的朋友可以參考下2015-07-07減小Maven項(xiàng)目生成的JAR包體積實(shí)現(xiàn)提升運(yùn)維效率
在Maven構(gòu)建Java項(xiàng)目過程中,減小JAR包體積可通過排除不必要的依賴和使依賴jar包獨(dú)立于應(yīng)用jar包來實(shí)現(xiàn),在pom.xml文件中使用<exclusions>標(biāo)簽排除不需要的依賴,有助于顯著降低JAR包大小,此外,將依賴打包到應(yīng)用外,可減少應(yīng)用包的體積2024-10-10