【解決方案】Java 互聯網項目如何防止集合堆記憶體溢出(一)

来源:https://www.cnblogs.com/CodeBlogMan/p/18022444
-Advertisement-
Play Games

OOM 幾乎是筆者工作中遇到的線上 bug 中最常見的,一旦平時正常的頁面線上上出現頁面崩潰或者服務無法調用,查看伺服器日誌後你很可能會看到“Caused by: java.lang.OutOfMlemoryError: Java heap space” 這樣的提示,那麼毫無疑問表示的是 Java ... ...


目錄

前言

OOM 幾乎是筆者工作中遇到的線上 bug 中最常見的,一旦平時正常的頁面線上上出現頁面崩潰或者服務無法調用,查看伺服器日誌後你很可能會看到“Caused by: java.lang.OutOfMlemoryError: Java heap space” 這樣的提示,那麼毫無疑問表示的是 Java 堆記憶體溢出了。

其中又當屬集合記憶體溢出最為常見。你是否有過把整個資料庫表查出來的全欄位結果直接賦值給一個 List 對象?是否把未經過過濾處理的數據賦值給 Set 對象進行去重操作?又或者是在高併發的場景下創建大量的集合對象未釋放導致 JVM 無法自動回收?

Java 堆記憶體溢出

我的解決方案的核心思路有兩個:一是從代碼入手進行優化;二是從硬體層面對機器做合理配置。


一、代碼優化

下麵先說從代碼入手怎麼解決。

1.1Stream 流自分頁

/**
 * 以下示例方法都在這個實現類里,包括類的繼承和實現
 */
@Service
public class StudyServiceImpl extends ServiceImpl<StudyMapper, Study> implements StudyService{}

在迴圈里使用 Stream 流的 skip()+limit() 來實現自分頁,直至取出所有數據,不滿足條件時終止迴圈

    /**
     * 避免集合記憶體溢出方法(一)
     * @return
     */
    private List<StudyVO> getList(){
        ArrayList<StudyVO> resultList = new ArrayList<>();
        //1、資料庫取出源數據,註意只拿 id 欄位,不至於溢出
        List<String> idsList = this.list(new LambdaQueryWrapper<Study>()
                                        .select(Study::getId)).stream()
                                        .map(Study::getId)
                                        .collect(Collectors.toList());
        //2、初始化迴圈
        boolean loop = true;
        long number = 0;
        long perSize = 5000;
        while (loop){
            //3、skip()+limit()組合,限制每次只取固定數量的 id
            List<String> ids = idsList.stream()
                                      .skip(number * perSize)
                                      .limit(perSize)
                                      .collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(ids)){
                //根據第3步的 id 去拿資料庫的全欄位數據,這樣也不至於溢出,因為一次只是 5000 條
                List<StudyVO> voList = this.listByIds(ids).stream()
                        .map(e -> e.copyProperties(StudyVO.class))
                        .collect(Collectors.toList());
                //addAll() 方法也比較關鍵,快速地批量添加元素,容量是比較大的
                resultList.addAll(voList);
            }
            //4、判斷是否跳出迴圈
            number++;
            loop = ids.size() == perSize;
        }
        return resultList;
    }

1.2資料庫分頁

這裡是用資料庫語句查詢符合條件的指定條數,迴圈查出所有數據,不滿足條件就跳出迴圈

    /**
     * 避免集合記憶體溢出方法(二)
     * @param param
     * @return
     */
    private List<StudyVO> getList(String param){
        ArrayList<StudyVO> resultList = new ArrayList<>();
        //1、構造查詢條件
        String id = "";
        //2、初始化迴圈
        boolean loop = true;
        int perSize = 5000;
        while (loop){
            //分頁,固定每次迴圈都查 5000 條
            Page<Study> studyPage = this.page(new Page<>
                                    (NumberUtils.INTEGER_ZERO, perSize), 
                                     wrapperBuilder(param, id));
            if (Objects.nonNull(studyPage)){
                List<Study> studyList = studyPage.getRecords();
                if (CollectionUtils.isNotEmpty(studyList)){
                    //3、每次截取固定數量的標識,數組下標減一
                    id = studyList.get(perSize - NumberUtils.INTEGER_ONE).getId();
                    //4、判斷是否跳出迴圈
                    loop = studyList.size() == perSize;
                    //添加進返回的 VO 集合中
                    resultList.addAll(studyList.stream()
                                      .map(e -> e.copyProperties(StudyVO.class))
                                      .collect(Collectors.toList()));
                }
                else {
                    loop = false;
                }
            }
        }
        return resultList;
    }

    /**
     * 條件構造
     * @param param
     * @param id
     * @return
     */
    private LambdaQueryWrapper<Study> wrapperBuilder(String param, String id){
        LambdaQueryWrapper<Study> wrapper = new LambdaQueryWrapper<>();
        //只查部分欄位,按照 id 的降序排列,形成順序
        wrapper.select(Study::getUserAvatar)
                .eq(Study::getOpenId, param)
                .orderByAsc(Study::getId);
        if (StringUtils.isNotBlank(id)){
            //這步很關鍵,只查比該 id 值大的數據
            wrapper.gt(Study::getId, id);
        }
        return wrapper;
    }

1.3其它思考

以上從根本上還是解決不了記憶體里處理大量數據的問題,取出 50w 數據放記憶體的風險就很大了。以下是我的其它解決思路:

  • 從業務上拆解:明確什麼情況下需要後端處理這麼多數據?是否可以考慮在業務流程上進行拆解?或者用其它形式的頁面交互代替?
  • 資料庫設計:數據一般都來源於資料庫,庫/表設計的時候儘量將表與表之間解耦,表欄位的顆粒度放細,即多表少欄位,查詢時只拿需要的欄位;
  • 數據放在磁碟:比如放到 MQ 里存儲,然後取出的時候註意按固定數量批次取,並且註意釋放資源;
  • 非同步批處理:如果業務對實時性要求不高的話,可以非同步批量把數據添加到文件流里,再存入到 OSS 中,按需取用;
  • 定時任務處理:詢問產品經理該功能或者實現是否是結果必須的?是否一定要同步處理?可以考慮在一個時間段內進行多次操作,緩解大數據量的問題;
  • 咨詢大數據團隊:尋求大數據部門團隊的專業支持,對於處理海量數據他們是專業的,看能不能提供一些可參考的建議。

二、硬體配置

核心思路:加大伺服器記憶體,合理分配伺服器的堆記憶體,並設置好彈性伸縮規則,當觸發告警時自動伸縮擴容,保證系統的可用性。

2.1雲伺服器配置

以下是阿裡雲 ECS 管理控制台的編輯頁面,可以對 CPU 和記憶體進行配置。在 ECS 實例伸縮組創建完成後,即可以根據業務規模去創建一個自定義伸縮配置,在業務量大的時候會觸發自動伸縮。

阿裡雲 ECS 管理

如果是部署在私有雲伺服器,需要對具體的 JVM 參數進行調優的話,可能還得請團隊的資深大佬、或者運維團隊的老師來幫忙處理。


三、文章小結

本篇文章主要是記錄一次線上 bug 的處理思路,在之後的文章中我會分享一些關於真實項目中處理高併發、緩存的使用、非同步/解耦等內容,敬請期待。

那麼今天的分享到這裡就結束了,如有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區交流!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Java 面向對象編程 面向對象編程 (OOP) 是一種編程範式,它將程式組織成對象。對象包含數據和操作數據的方法。 OOP 的優勢: 更快、更易於執行 提供清晰的結構 代碼更易於維護、修改和調試 提高代碼重用性 減少開發時間 類和對象 類 是對象的模板,它定義了對象的屬性和方法。 對象 是類的實例 ...
  • Miniconda是Anaconda的簡化版, 可以管理多個Python版本的環境. 實際使用的話, 占用的空間不會很小, 我跑一些正常的應用後, 安裝目錄占用空間4.3GB, 安裝建議要預留10到20G的空間. 安裝 Miniconda 下載安裝包 https://docs.anaconda.co ...
  • 美團面試:Kafka如何處理百萬級消息隊列? 在今天的大數據時代,處理海量數據已成為各行各業的標配。特別是在消息隊列領域,Apache Kafka 作為一個分散式流處理平臺,因其高吞吐量、可擴展性、容錯性以及低延遲的特性而廣受歡迎。但當面對真正的百萬級甚至更高量級的消息處理時,如何有效地利用 Kaf ...
  • 摘要 我們報告了 GPT-4 的開發,這是一個大規模、多模態的模型,可以接受圖像和文本輸入,並生成文本輸出。雖然在許多現實場景中不如人類,但 GPT-4 在各種專業和學術基準測試中表現出與人類水平相當的性能,包括在模擬的律師資格考試中取得了約前10%的考生得分。 GPT-4 是基於 Transfor ...
  • 虛擬線程(Virtual Threads)是 Java 21 所有新特性中最為吸引人的內容,它可以大大來簡化和增強Java應用的併發性。但是,隨著這些變化而來的是如何最好地管理此吞吐量的問題。本文,就讓我們看一下開發人員在使用虛擬線程時,應該如何管理吞吐量。 在大多數情況下,開發人員不需要自己創建虛 ...
  • 首先,跨域的域是什麼? 跨域的英文是:Cross-Origin。 Origin 中文含義為:起源,源頭,出生地。 在跨域中,"域"指的是一個 Web 資源(比如網頁、腳本、圖片等)的源頭。 包括該資源的協議、主機名、埠號。 在同源策略中,如果兩個資源的域相同,則它們屬於同一域,可以自由進行交互和共 ...
  • 通過使用Python編程語言,編寫腳本來自動化Excel和CSV之間的轉換過程,可以批量處理大量文件,定期更新數據,並集成轉換過程到自動化工作流程中。本文將介紹如何使用第三方庫Spire.XLS for Python 實現: 使用Python將Excel轉為CSV 使用Python 將CSV轉為Ex ...
  • 多年不用PageHelper了,最近新入職的公司,採用了此工具集成的框架,作為一個獨立緊急項目開發的基礎。項目開發起來,還是手到擒來的,但是沒想到,最終測試的時候,深深的給我上了一課。 我的項目發生了哪些奇葩現象? 一切的問題都要從我接受的項目開始說起, 在開發這個項目的過程中,發生了各種奇葩的事情 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...