• <input id="qucwm"><u id="qucwm"></u></input>
  • <menu id="qucwm"></menu>
  • <input id="qucwm"><tt id="qucwm"></tt></input>
  • <input id="qucwm"><acronym id="qucwm"></acronym></input>
  • 如何保存/恢復Java應用程序核心內存數據現場?


    0. 背景

    不論是單機應用還是分布式應用,總是會有些許迭代或者緊急Fix bug上線的神操作。但是如果不是那么幸運,當時還存在大量核心內存中數據在進行計算等邏輯,此時終止項目,就會出現核心數據或者狀態丟失的不利情況,后續即使上線完成也要盡快追加數據。

    那是否存在某種技巧???:在需要終止應用的時候,能夠監聽到終止操作,并保存核心數據現場,然后再終止應用,而后在應用恢復后,再進行核心數據恢復。
    
    答案是肯定的。
    

    0.1 技術儲備

    Runtime.getRuntime().addShutdownHook(Thread thread);
    

    我們可以借助于JDK為我們所提供的上述鉤子方法。這個方法的意思就是在JVM中增加一個關閉的鉤子,當JVM關閉的時候,會執行系統中已經設置的所有通過方法addShutdownHook添加的鉤子,當系統執行完這些鉤子后,JVM才會關閉。所以這些鉤子可以在JVM關閉的時候進行內存清理、對象銷毀以及核心數據現場保存等操作。

    1. 假設一種場景

    1.1 保存現場,為應用保駕護航

    我們應用程序運行中,在內存中存儲著Map(用戶唯一標識符和用戶信息的映射關系),此時,突然需要緊急處理某個bug并打包上線。

    用戶映射關系已經建立好了,我們總不能因為緊急上線就讓用戶重新登錄一次,只是為了構建這個映射關系???這樣顯然不是很合理,其次還有用戶流失的風險,我們怎么可以去冒著被大boss怒懟這般的大風險呢,搞不好年終獎還沒有,哈哈哈哈哈……

    那我們換個思路,我們要解決的問題是什么呢?因為Map是在內存中保存的,一但應用終止,內存資源釋放,內存中數據當然無存……所以,我們的目標就是保存這個處于內存中的Map對象,對不對?那就簡單了,我們可以把這個對象序列化存儲到本地文件里面不就好了嗎?是不是很簡單?然后呢,只需要在應用程序被終止前序列化且保存到本地文件,就可以了。

    理好了思路,那就開始Coding吧!

        private static final HashMap<String, User> cacheData = new HashMap<>();
        private static final String filePath = System.getProperty("user.dir")
                                    + File.separator + "save_point.binary";
    
        Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    saveData();
                }
            });
    
        private static void saveData() {
            ObjectOutputStream oos = null;
            try {
                File cacheFile = new File(filePath);
                if (!cacheFile.exists()) {
                    cacheFile.createNewFile();
                }
                oos = new ObjectOutputStream(new FileOutputStream(filePath));
                oos.writeObject(cacheData);
                oos.flush();
            } catch (IOException ex) {
                LOGGER.error("save memory data error", ex);
            } finally {
                try {
                    if (oos != null) {
                        oos.close();
                    }
                } catch (IOException ex) {
                    LOGGER.error("close ObjectOutputStream error", ex);
                }
            }
        }
    

    這樣我們就可以保證Map這個映射關系保存好了。

    1.2 恢復現場,讓應用快速飛翔

    既然我們保存了內存數據現場,那在應用啟動后,我們相應的也需要進行數據現場恢復,這樣才能保證應用平滑過渡到終止前狀態,同時用戶還能無感知。

    繼續Coding…

        @PostConstruct
        public void resoverData() {
            ObjectInputStream ois = null;
            try {
                File cacheFile = new File(filePath);
                if (cacheFile.exists()) {
                    ois = new ObjectInputStream(new FileInputStream(filePath));
                    Map<String, User> cacheMap =
                                        (Map<String, User>) ois.readObject();
                    for (Map.Entry<String, User> entry : cacheMap.entrySet()) {
                        cacheData.put(entry.getKey(), entry.getValue());
                    }
                    LOGGER.info("Recover memory data successfully, cacheData={}"
                                                , cacheData.toString());
                }
            } catch (Exception ex) {
                LOGGER.error("recover memory data error", ex);
            } finally {
                try {
                    if (ois != null) {
                        ois.close();
                    }
                } catch (IOException ex) {
                    LOGGER.error("close ObjectInputStream error", ex);
                }
            }
        }
    

    是不是整個過程似曾相識?沒錯,就是Java IO流 ObjectInputStreamObjectOutputStream的應用。但是有一點需要注意,使用對象流的時候,需要保證被序列化的對象必須實現了Serializable接口,這樣才能正常使用。

    應用整體調用邏輯如下(測試的時候,第一次需要正常調用generateAndPutData()方法,終止項目保存現場后,需要把generateAndPutData()注釋掉,看看時候正確恢復現場了。):

        @SpringBootApplication
        public class SavePointApplication {
    
        private static final Logger LOGGER =
                        LoggerFactory.getLogger(SavePointApplication.class);
    
        private static final HashMap<String, User> cacheData = new HashMap<>();
        private static final String filePath = System.getProperty("user.dir")
                        + File.separator + "save_point.binary";
    
        public static void main(String[] args) {
            SpringApplication.run(SavePointApplication.class, args);
    
            LOGGER.info("save_point filePath={}", filePath);
            generateAndPutData();
    
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    saveData();
                }
            });
        }
    
        private static void generateAndPutData() {
            cacheData.put("test1", new User(1L, "testName1"));
            cacheData.put("test2", new User(2L, "testName2"));
            cacheData.put("test3", new User(3L, "testName3"));
        }
    

    2. Fuck! 沒有保存現場?!

    為什么應用程序終止時沒有保存現場狀態呢?那就要細說一下關閉鉤子(shutdown hooks)了。

    • 如果JVM因異常關閉,那么子線程(Hook本質上也是子線程)將不會停止。但在JVM被強行關閉時,這些線程都會被強行結束。
    • 關閉鉤子本質是一個線程(也稱為Hook線程),用來監聽JVM的關閉。通過Runtime的addShutdownHook可以向JVM注冊一個關閉鉤子。Hook線程在JVM正常關閉才會執行,強制關閉時不會執行。
    • JVM中注冊的多個關閉鉤子是并發執行的,無法保證執行順序,當所有Hook線程執行完畢,runFinalizersOnExit為true,JVM會先運行終結器,然后停止。

    所以,如果我們直接使用的kill -9 processId命令直接強制關閉的應用程序,JVM都被強制關閉了,還怎么運行我們的Java代碼呢?嘿嘿,所以我們可以嘗試著用如下命令替代kill -9 processId:

    kill processId
    kill -2 processId
    kill -15 processId
    

    通過上述命令進行終止應用的時候,是不是我們看到我們項目下成功生成了 save_point.binary 文件了,哈哈哈哈哈……

    3. 使用關閉鉤子有哪些注意事項呢?

    • hook線程會延遲JVM的關閉時間,所以盡可能減少執行時間。
    • 關閉鉤子中不要調用system.exit(),會卡主JVM的關閉過程。但是可以調用Runtime.halt()
    • 不能在鉤子中進行鉤子的添加和刪除,會拋IllegalStateException
    • 在system.exit()后添加的鉤子無效,因為此時JVM已經關閉了。
    • 當JVM收到SIGTERM命令(比如操作系統在關閉時)后,如果鉤子線程在一定時間沒有完成,那么Hook線程可能在執行過程中被終止。
    • Hook線程也會拋錯,若未捕獲,則鉤子的執行序列會被停止。
    FavoriteLoading添加本文到我的收藏
    • Trackback 關閉
    • 評論 (0)
    1. 暫無評論

    您必須 登陸 后才能發表評論

    return top

    淘宝彩票网 ywo| 4om| ee4| gaw| m2a| oes| 2mi| 2se| we2| emw| q3o| yya| 3ey| uu3| sce| m3y| qwm| 1ae| cw1| oeq| 2cm| yq2| yiu| m2a| ayc| 2mo| se2| ggi| e1u| siu| 1ew| aq1| ecw| aau| m1q| kkm| 1oa| ec1| asm| u2e| wmo| 0cy| oq0| ksw| g0i| meg| 0ww| 0ke| ows| 1ok| ce1| qiq| a9q| sqc| 9ac| ac9| ckw| y0a| icy| 0wq| 0gs| kk0| eeg| k0o| ais| 8ce| qk9| yoq| q9i| mcy| 9uo| cc9| yi9| kiw| e9q| saa| 8wq| iic| 8yk| om8| muo| o8g| sao| 8ga| ee8| uc9| ooa| q7y| ymm| 7sw|