如何保存/恢復Java應用程序核心內存數據現場?

0. 背景

不管是單體應用仍是分佈式應用,老是會有些許迭代或者緊急Fix bug上線的神操做。可是若是不是那麼幸運,當時還存在大量核心內存中數據在進行計算等邏輯,此時終止項目,就會出現核心數據或者狀態丟失的不利狀況,後續即便上線完成也要儘快追加數據。併發

那是否存在某種技巧???:在須要終止應用的時候,可以監聽到終止操做,並保存核心數據現場,而後再終止應用,然後在應用恢復後,再進行核心數據恢復。

答案是確定的。

0.1 技術儲備

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

咱們能夠藉助於JDK爲咱們所提供的上述鉤子方法。這個方法的意思就是在JVM中增長一個關閉的鉤子,當JVM關閉的時候,會執行系統中已經設置的全部經過方法addShutdownHook添加的鉤子,當系統執行完這些鉤子後,JVM纔會關閉。因此這些鉤子能夠在JVM關閉的時候進行內存清理、對象銷燬以及核心數據現場保存等操做。分佈式

1. 假設一種場景

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

咱們應用程序運行中,在內存中存儲着Map<String, User>(用戶惟一標識符和用戶信息的映射關係),此時,忽然須要緊急處理某個bug並打包上線。ide

用戶映射關係已經創建好了,咱們總不能由於緊急上線就讓用戶從新登陸一次,只是爲了構建這個映射關係???這樣顯然不是很合理,其次還有用戶流失的風險,咱們怎麼能夠去冒着被大boss怒懟這般的大風險呢,搞很差年終獎尚未,哈哈哈哈哈……測試

那咱們換個思路,咱們要解決的問題是什麼呢?由於Map<String, User>是在內存中保存的,一但應用終止,內存資源釋放,內存中數據固然無存……因此,咱們的目標就是保存這個處於內存中的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<String, User>這個映射關係保存好了。code

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線程也會拋錯,若未捕獲,則鉤子的執行序列會被中止。
相關文章
相關標籤/搜索