歡迎轉(zhuǎn)載,但請(qǐng)?jiān)陂_頭或結(jié)尾注明原文出處【blog.chaosjohn.com】
背景
公司某項(xiàng)目的后端技術(shù)棧采用的是 SpringBoot + Kotlin,具體細(xì)節(jié)本文不作展開。
業(yè)務(wù)中,用戶在我們平臺(tái)上購(gòu)買產(chǎn)品,我們通過實(shí)時(shí)請(qǐng)求供應(yīng)商的交易API,在商戶余額(即我們?cè)诠?yīng)商那邊的儲(chǔ)值)里扣除對(duì)應(yīng)的金額后,才會(huì)將產(chǎn)品的“源文件”返回,進(jìn)而交付給用戶。
項(xiàng)目迭代中我們需要不斷的部署新的版本上線,最初的做法很暴力,構(gòu)建新的 flatjar 運(yùn)行起來(lái),然后直接結(jié)束掉 舊的 java 進(jìn)程。
然后就發(fā)現(xiàn)問題了:某次購(gòu)買行為中,交易API的請(qǐng)求發(fā)送出去了,還沒等待請(qǐng)求返回,進(jìn)程就被殺死了,造成儲(chǔ)值余額被扣了,但是“源文件”并沒有拿到,而供應(yīng)商的API又存在延遲,即交易API處理成功,但是通過訂單查詢API卻查不到。
所以,如何才能在 java 進(jìn)程被殺死的時(shí)候,做完 善后工作 再退出呢?
解決
一般我們殺死進(jìn)程,都是給進(jìn)程發(fā)送信號(hào) Signal:
SIGINTSIGTERM
這里我們用到兩個(gè)類:
-
sun.misc.Signal,代表信號(hào) -
sun.misc.SignalHandler,用來(lái)處理進(jìn)程接收到的信號(hào)
同時(shí),我們?cè)O(shè)計(jì)以下全局變量:
-
var killSignalReceived = false// 用來(lái)表示是否接收到終止信號(hào) -
val jobSet = mutableSetOf<String>()// 表示需要在結(jié)束之前等待完成的任務(wù)
所以交易API的請(qǐng)求處理,我們將改成:
if (!killSignalReceived) { // 只有為 `false`,才進(jìn)行交易處理
synchronized(jobSet) {
jobSet.add(jobTitle) // 處理之前,將當(dāng)前處理任務(wù)存入 `jobSet`
}
// Todo: 具體實(shí)現(xiàn)請(qǐng)求交易API的處理
synchronized(jobSet) {
jobSet.remove(jobTitle) // 處理結(jié)束,將當(dāng)前處理任務(wù)從 `jobSet` 中移除
}
}
在程序主函數(shù)中,新增:
val killHandler = SignalHandler {
logger.error("intercept signal of ${it.toJson()}")
killSignalReceived = true
while (jobSet.isNotEmpty()) {
logger.error("Background Jobs: \n\t\t" + jobSet.joinToString("\n\t\t") + "\n")
Thread.sleep(1000)
}
exitProcess(0)
}
Signal.handle(Signal("INT"), killHandler)
Signal.handle(Signal("TERM"), killHandler)
當(dāng) java 進(jìn)程接收到 SIGINT 和 SIGTERM 信號(hào)后:
- 將
killSignalReceived置為true - 循環(huán)檢查
jobSet是否為空- 不為空,打印當(dāng)前未結(jié)束的任務(wù)列表,等待1s后再次檢查
- 為空,程序退出
如果需要忽略 善后工作 強(qiáng)行退出,給進(jìn)程發(fā)送 SIGKILL 即可:
kill -KILL pidkill -9 pid