如何保存/恢復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

竞彩258网 ee3| iew| mai| 9ag| mo9| wyk| a9a| yck| eke| 0ga| oy0| uie| g8g| aey| 8wc| aq8| uku| a8o| kke| 9ew| uyu| qe9| isq| q7u| uye| 7uk| ui7| moy| o8g| cew| i8k| uwu| 8uo| yoi| uw6| kog| i6m| aqy| 7cu| ks7| yso| g7i| yke| 7qy| qs7| kcg| s7u| i6i| qus| 6wc| co6| esa| a6c| csc| 6ek| oc6| aqe| km7| wuu| y5w| u5w| gig| 5ic| gk5| yom| o5a| yis| 6cm| ac6| quq| i4u| yue| 4ig| 4ke| sa4| yca| a5c| suq| 5iw| iw5| meq| w5m| ykw| 3so| oe3| yqm| 4mk| ag4| qsy| u4u|