與其碌碌無為,不如興風作浪。
雖然不是所有的系統都需要很多的并發編程技術,但是掌握常見的高并發秘籍,便能讓我們的系統快起來,面對訪問量的劇增從容應對。
接下來,為我們一起來看看常見的高并發技術有哪些。總結起來,主要包括緩存、限流、熔斷降級、異步、池化、代碼優化、JVM調優、預熱、等。
緩存
主要包括本地緩存和分布式緩存。可以使用Redis和Caffeine等方式緩存數據,一般情況下分布式系統使用分布式緩存就可以了。對于高并發的系統,可以考慮多級緩存,但是要考慮內存以及數據一致性問題。
本地緩存:解決redis的熱key問題和提升性能;
分布式緩存:解決緩存容量和提升性能。
本地緩存
雖然redis號稱單節點能抗住10Wqps,但是開發過程中為了保險,會降低預期。那么如何發現這些熱點key呢?可以在開發的時候憑業務經驗估計,比如秒殺的商品信息。上線后,也可以隨時在客戶端進行收集。
Caffeine是基于java8實現的新一代緩存工具,緩存性能接近理論最優。可以看作是Guava Cache的增強版,功能上兩者類似,不同的是Caffeine采用了一種結合LRU、LFU優點的算法:W-TinyLFU,在性能上有明顯的優越性。
使用方式如下:
public class CaffeineCacheTest {public static void main(String[] args) throws Exception {//創建guava cacheCache<String, String> loadingCache = Caffeine.newBuilder()//cache的初始容量.initialCapacity(5)//cache最大緩存數.maximumSize(10)//設置寫緩存后n秒鐘過期.expireAfterWrite(17, TimeUnit.SECONDS)//設置讀寫緩存后n秒鐘過期,實際很少用到,類似于expireAfterWrite//.expireAfterAccess(17, TimeUnit.SECONDS).build();String key = "key";// 往緩存寫數據loadingCache.put(key, "value");// 獲取value的值,如果key不存在,獲取value后再返回String value = loadingCache.get(key, CaffeineCacheTest::getValueFromDB);// 刪除keyloadingCache.invalidate(key);}private static String getValueFromDB(String key) {return "value";}
}
分布式緩存
也就是引入redis中間件進行數據緩存。
使用Spring Cache進行商品類目數據緩存:
/*** 商品類目*/
@DubboService
@CacheConfig(cacheNames = CACHE_NAME_CATEGORY)
@Slf4j
public class CategoryFacadeServiceImpl implements ICategoryFacadeService {// 使用@Cacheable注解,它會將方法返回結果存儲在注解指定的緩存中@Override@Cacheablepublic List<CategoryResp> listAllCategory() throws ServiceException {log.info("查詢所有類目開始");return allCategory;}// @CacheEvict 注解來表示刪除一個、多個或所有的值,以刷新緩存@Override@Transactional(rollbackFor = Exception.class)@CacheEvict(cacheNames = CACHE_NAME_CATEGORY, allEntries = true)public Long saveAndUpdate(@NotNull CategoryReq req) throws ServiceException {log.info("新增或修改類目開始CategoryReq{}", req);return result;}@Override@Transactional(rollbackFor = Exception.class)@CacheEvict(cacheNames = CACHE_NAME_CATEGORY, allEntries = true)public boolean deleteCategory(@NotNull Long id) throws ServiceException {log.info("開始刪除類目");return flag&&mappingFlag;}
}
多級緩存
通過新增本地緩存,可用使得流量在應用層直接返回,避免進一步訪問Redis。大大提高數據讀取的效率,但是成本也是很高的。
- 內存要求:需要在應用服務器同于數據,需要提高應用服務器的內存;
- 數據一致性:需要保證多級緩存之間,各個本地緩存之間數據的一致性。
需要重具體的業務場景觸發,兼顧以下三個方面考慮是否需要本地緩存:
- 并發量
- 內存情況
- 變更是否頻繁
限流
限流是為了保護系統的可用性而做出的一種妥協,通過減低請求成功的數量,保證重要功能的可用。需要通過壓測預估系統可承載的并發量。在系統資源緊張的情況下,保證系統的可用性。
可通過sentinel實現限流,經典算法包括令牌桶,漏桶,滑動時間窗口等。
熔斷降級
熔斷降級是分布式系統中常用的兩種保護機制,旨在提高系統的穩定性和可用性。以下是關于熔斷降級的詳細解釋:
熔斷
定義:熔斷類似于電路中的保險絲,當某個異常條件被觸發時,直接熔斷整個服務,防止系統因某個服務的故障而導致整體服務失敗。
觸發條件:熔斷的觸發條件通常與服務調用的失敗率、請求超時等有關。例如,在Spring Cloud的Hystrix組件中,如果檢測到10秒內請求的失敗率超過50%,則觸發熔斷機制。
作用:熔斷機制通過快速失敗和快速恢復,防止在復雜分布式系統中出現級聯故障,從而提高系統的整體彈性。
降級
定義:降級是指在系統壓力劇增或出現故障時,根據當前業務情況及流量,對一些服務和頁面進行有策略的降級,以此緩解服務器資源的壓力,保證核心業務的正常運行。
觸發條件:降級的觸發條件通常包括服務超時、失敗次數、故障、限流等。在Hystrix中,降級可以在方法拋出異常、方法調用超時、熔斷器開啟攔截調用、線程池或隊列或信號量已滿等情況下觸發。
目的:降級的目的是在系統出現問題時,仍能保證有限功能可用,提供一種退而求其次的解決方案。例如,在電商交易系統中,當系統負載過高時,可以開啟降級功能,優先保證支付功能可用,而其他非核心功能如評論、物流、商品介紹等可以暫時關閉。
熔斷與降級的區別
概念不同:熔斷是當服務出現故障時,直接熔斷整個服務,防止系統因某個服務的故障而導致整體服務失敗;而降級是在系統壓力劇增或出現故障時,通過關閉部分非核心功能來保證核心業務的正常運行。
觸發條件不同:熔斷的觸發條件通常與服務調用的失敗率、請求超時等有關;而降級的觸發條件則包括服務超時、失敗次數、故障、限流等。
歸屬關系不同:熔斷時可能會調用降級機制,因為熔斷是從全局出發,為了保證系統穩定性而停用服務;而降級是退而求其次,提供一種保底的解決方案。
常見的降級類型包括:大促非核心的接口降級;日志降級等。
異步
主要涉及到如何讓程序在執行某些可能耗時的操作時,不會阻塞主線程,從而提高程序的響應性和性能。主要策略包括多線程和消息隊列等。
系統解耦:對于不需要客戶感知結果的部分,可通過消息的方式完成后續處理,完成后通知客戶結果。不需要阻塞客戶一直等待。
提升性能:對于一些查詢操作,可能設置多個獨立的數據內容,這種情況可以使用CompletableFuture進行一步任務編排,提升查詢效率。
下面是通過任務編排實現商品屬性查詢的案例:
CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {System.out.println("查詢商品的圖片信息");return "hello.jpg";
});CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {System.out.println("查詢商品的屬性");return "黑色";
});CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println("查詢商品介紹");return "華為";
});// 等待全部執行完
// CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);
// allOf.get();// 只需要有一個執行完
CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get();
System.out.println("main....end....." + anyOf.get());
池化技術
池化技術主要用于避免頻繁創建和銷毀昂貴資源(如數據庫連接、線程、大對象等)所帶來的性能開銷。通過預先創建一定數量的資源并維護在一個池中,當需要時從池中獲取資源使用,使用完畢后歸還到池中而不是直接銷毀,可以顯著提高應用程序的效率和響應速度。
下面是一些典型的Java池化技術實例:
- 數據庫連接池:
- 實現庫:Apache DBCP, C3P0, HikariCP, Tomcat JDBC Pool等。
- 作用:管理數據庫連接,避免了每次數據庫操作都新建和銷毀連接的開銷。
- 線程池:
- 實現庫:java.util.concurrent.ExecutorService接口及其實現類,如ThreadPoolExecutor。
- 作用:預先創建一定數量的線程,將任務提交給線程池處理,提高了并發處理能力,同時也限制了系統創建過多線程導致的資源耗盡風險。
- 對象池:
- 實現庫:Apache Commons Pool。
- 作用:適用于任何需要重用的對象,如大對象、網絡連接、圖形對象等,減少垃圾回收壓力和創建對象的成本。
- 緩沖池(Buffer Pool):
- 在某些IO密集型應用中,如NIO(非阻塞I/O)編程,通過重用緩沖區可以提升讀寫效率。
使用池化技術的關鍵在于合理配置池的大小,過大可能導致資源浪費,過小則可能因為資源爭搶而影響性能。此外,池化技術還需要考慮資源的分配與回收策略、超時處理、異常處理等機制,確保資源的有效管理和利用。
代碼優化
通過優化代碼邏輯,減少調用鏈路,減少數據庫查詢次數,提前校驗流程。
JVM調優
主要是系統根據業務情況和機器配置設置合理的JVM參數。
預熱
通過執行定時任務等方式提前將數據加載到緩存。通過預熱,能夠避免緩存剛開始沒有數據時全量請求查數據庫的行為。
如果秒殺要走正常的加入購物車流程,然后去來鎖庫存,最終去支付,這樣整個流程太慢了,在高并發系統里邊肯定會出現整個級聯崩潰的情況。我們應該先做到預熱庫存,比如現在要秒殺的商品,數量有400件,我們給 redis 里面存一個 400 的信號量,想要秒殺的人進來之后,必須要先拿到信號量,這一塊我們會對 redis 的信號量進行快速扣減,直接扣減1個數,所以無論有多少請求進來,即使有百萬請求,最終也只有 400個人能拿到這個信號量的值。然后我們會將這 400 個人放行給我們后臺的集群系統,這些請求即使走正常的下單邏輯,系統也不會出現什么問題。
如果是單臺機器,也可以使用一些簡單的辦法實現,但是不太靈活。比如:
監聽啟動事件
- 使用ApplicationListener監聽ContextRefreshEvent或ApplicationReadyEvent等應用上下文初始化完成事件,在這些事件觸發后執行數據加載的操作。
@Component
public class CacheWarmer implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {// 執行緩存預熱業務...cacheManager.put("key", dataList);}
}
@Component
public class CacheWarmer implements ApplicationListener<ApplicationReadyEvent> {@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// 執行緩存預熱業務...cacheManager.put("key", dataList);}
}
PostConstuct注解
該注解是Java jdk提供的注解,而不是Spring框架提供的。該注解的功能是當依賴注入完成后用于執行初始化的方法,并且只會被執行一次
@Component
public class CachePreloader {@Autowiredprivate YourCacheManager cacheManager;// 注解@PostConstructpublic void preloadCache() {// 執行緩存預熱業務...cacheManager.put("key", dataList);}
}
實現InitializingBean接口
InitializingBean是Spring提供的拓展性接口,為bean提供了屬性初始化后的處理方法,它唯一的方法便是afterPropertiesSet,凡是實現該接口的類,在bean屬性初始化后都會執行該方法。
@Component
public class CachePreloader implements InitializingBean {@Autowiredprivate YourCacheManager cacheManager;@Overridepublic void afterPropertiesSet() throws Exception {// 執行緩存預熱業務...cacheManager.put("key", dataList);}
}
hi,你好,我是松語。985軟件工程研究生畢業,一個工作三年的程序員。
這里有:
- 技術分享,包括編程技巧和踩坑記錄等;
- 求職經驗,校招、副業、社招跳槽經驗等;
- 詩和遠方。
親愛的你,切莫辜負有夢想的自己,萍水相逢的感情,以及良辰美景好時光。