一篇文章告訴你真實場景下服務端接口性能問題是如何解決的
作為Java后端開發(fā)者,我們創(chuàng)作的許多代碼直接影響著用戶的使用體驗。如果后端代碼性能不佳,用戶在訪問網(wǎng)站時就必須花費更多時間等待服務器響應。這可能引發(fā)用戶投訴甚至用戶流失問題。
性能優(yōu)化是一個廣泛而重要的話題?!禞ava程序性能優(yōu)化》提到性能優(yōu)化可分為五個層次:設計優(yōu)化、代碼優(yōu)化、JVM優(yōu)化、數(shù)據(jù)庫優(yōu)化、操作系統(tǒng)優(yōu)化等。每個層次都涵蓋許多方法論和最佳實踐。本文無意進行全面詳盡的概述,只是列舉幾個常用的Java代碼優(yōu)化方案,希望讀者閱讀后能實際應用到自己的代碼中。
單例層面
在處理IO操作、數(shù)據(jù)庫連接、配置文件解析加載等耗費大量系統(tǒng)資源的任務時,我們必須限制這些實例的創(chuàng)建,或者始終使用一個共享的實例,以節(jié)約系統(tǒng)資源。這種情況下就需要使用單例模式。
批量操作
若有100個請求,逐個執(zhí)行顯然效率較低。將這100個請求合并為一個請求進行批量操作,則能大幅提升效率。
特別是在數(shù)據(jù)庫操作中,批量處理不僅比逐條執(zhí)行效率更高,還能有效降低數(shù)據(jù)庫連接數(shù),提升應用的QPS上限。
Future模式處理
假設某項任務需花費一定時間執(zhí)行,為避免無謂的等待,可先獲取一個“提貨單”——即Future,隨后繼續(xù)處理其他任務,直至“貨物”抵達,即任務執(zhí)行完成并獲得結(jié)果。這時便可憑借“提貨單”提取物品,即通過Future對象獲取返回值。
偽代碼
public class RealData implements Callable<String> {
protected String data;
public RealData(String data) {
this.data = data;
}
@Override
public String call() throws Exception {
// 通過sleep方法演示業(yè)務是緩慢的
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
}
}
public class Application {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask =
new FutureTask<>(new RealData("name"));
ExecutorService executor =
Executors.newFixedThreadPool(1); // 使用線程池
// 執(zhí)行FutureTask,相當于上例中的client.request("name")發(fā)送請求
executor.submit(futureTask);
// 這里可以用一個sleep代替對其他業(yè)務邏輯的處理
// 在處理這些業(yè)務邏輯的同時,RealData也在創(chuàng)建,充分利用等待時間
Thread.sleep(2000);
// 使用真實數(shù)據(jù)
// 如果call()沒有執(zhí)行完成,仍會等待
System.out.println("數(shù)據(jù)=" + futureTask.get());
}
}
線程池思路
合理運用線程池帶來三大益處。首先,降低資源消耗:通過重復利用已創(chuàng)建的線程,降低線程的創(chuàng)建與銷毀成本。其次,提高響應速度:任務到達時,無需等待線程創(chuàng)建即可立即執(zhí)行。第三,提升線程可管理性:線程是珍貴資源,無節(jié)制地創(chuàng)建會消耗系統(tǒng)資源,降低系統(tǒng)穩(wěn)定性;線程池能實現(xiàn)統(tǒng)一分配、優(yōu)化和監(jiān)控。
自 Java 5 開始,引入了并發(fā)編程新API,如Executor框架,內(nèi)部采用線程池機制,位于java.util.concurrent包中。通過該框架控制線程的啟動、執(zhí)行和關(guān)閉,可簡化并發(fā)編程操作。
偽代碼
public class MultiThreadTest {
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
ExecutorService executor = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
});
System.out.println(" ===> Main Thread! ");
}
}
NIO處理
JDK自1.4版本起引入了新的I/O編程類庫,即NIO。NIO不僅帶來了高效的Buffer和Channel,還引入了基于Selector的非阻塞I/O機制,可以將多個異步I/O操作集中到一個或少數(shù)幾個線程中進行處理。使用NIO替代阻塞I/O能夠提高程序的并發(fā)吞吐能力,降低系統(tǒng)開銷。
針對每個請求,如果為其單獨開啟一個線程來處理邏輯,當客戶端數(shù)據(jù)傳輸是間歇性的而非連續(xù)的時,相應線程會處于I/O等待狀態(tài),并頻繁進行上下文切換。利用NIO引入的Selector機制,可以提升程序的并發(fā)效率,改善這種狀況。
偽代碼
public class NioTest {
static public void main( String args[] ) throws Exception {
FileInputStream fin = new FileInputStream("D:\\test.txt");
// 獲取通道
FileChannel fc = fin.getChannel();
// 創(chuàng)建緩沖區(qū)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 讀取數(shù)據(jù)到緩沖區(qū)
fc.read(buffer);
buffer.flip();
while (buffer.remaining()>0) {
byte b = buffer.get();
System.out.print(((char)b));
}
fin.close();
}
}
優(yōu)化鎖層面
在并發(fā)場景中,頻繁使用鎖是很常見的情況。然而,鎖引發(fā)競爭,而競爭又會耗費大量資源。那么,在Java代碼中,如何優(yōu)化鎖呢?我們可以考慮以下幾個方面:
- 縮短鎖持有時間
- 嘗試使用同步代碼塊替代同步方法,從而減少鎖的占用時間。
- 降低鎖粒度
在并發(fā)環(huán)境中使用Map時,最好選用ConcurrentHashMap替代HashTable和HashMap(ConcurrentHashMap采用分段鎖,鎖的粒度更細)。
分離鎖
普通鎖(例如synchronized)可能導致讀寫互相阻塞,可以嘗試將讀操作和寫操作分開。
鎖粗化
有時我們希望將多次鎖的請求合并成一個,以減少頻繁加鎖、同步和解鎖所帶來的性能損失。
鎖消除
鎖消除是指Java虛擬機在JIT編譯時,經(jīng)過運行上下文的掃描,去除那些不會產(chǎn)生共享資源競爭的鎖。通過鎖消除,可以減少無謂的鎖請求時間。
數(shù)據(jù)傳輸壓縮
在數(shù)據(jù)傳輸之前,壓縮數(shù)據(jù)是一種優(yōu)化方式,可以減少網(wǎng)絡傳輸?shù)臄?shù)據(jù)量,提升傳輸速度。接收端可解壓數(shù)據(jù),還原傳輸內(nèi)容。壓縮后的數(shù)據(jù)能節(jié)省存儲介質(zhì)(如磁盤或內(nèi)存)空間和網(wǎng)絡帶寬,從而降低成本。然而,壓縮并非無成本之舉。數(shù)據(jù)壓縮需要大量CPU計算,并且根據(jù)壓縮算法的不同,計算復雜度和壓縮比都有顯著差異。通常需要根據(jù)業(yè)務情景選擇合適的壓縮算法。
緩存計算結(jié)果
對于相同的用戶請求,若每次都重復查詢數(shù)據(jù)庫、重復計算,將浪費大量時間和資源。將計算結(jié)果緩存至本地內(nèi)存或使用分布式緩存,可節(jié)約寶貴的CPU計算資源,減少數(shù)據(jù)庫重復查詢或磁盤I/O。將原本需要磁頭物理轉(zhuǎn)動的操作轉(zhuǎn)化為內(nèi)存中的電子運動,提高響應速度。同時,快速釋放線程也提升了應用的吞吐能力。