團購訂單到了截止時間,系統會自動結算:先把訂單從 OPEN 改成 CLOSED,再向參與者扣款,問題是:「如果結算做到一半失敗了,比方說資料庫突然斷線,下一次重試要從哪裡開始?」這時扣款交易會 rollback,但前面的 CLOSED 訂單狀態已經寫進資料庫,要讓下一次重試知道:「截止已經完成了,訂單狀態已經更新成CLOSED,應該直接從扣款繼續。」
CLOSED 擋住目前 settleOrder 原本只接受 OPEN:
if (order.getStatus() != OrderStatus.OPEN) {
return;
}
因此上面的訂單重新執行時,看到自己已經是 CLOSED,就直接返回,也因為沒有拋例外,消費端也不會再次把它放回佇列,最後訂單就永遠停在 CLOSED。
因此 settleOrder 必須同時接受 OPEN 和 CLOSED:
if (order == null
|| (order.getStatus() != OrderStatus.OPEN
&& order.getStatus() != OrderStatus.CLOSED)) {
return;
}
if (order.getStatus() == OrderStatus.OPEN) {
if (orderMapper.updateStatusIfIn(
orderId, OrderStatus.CLOSED, List.of(OrderStatus.OPEN)) == 0) {
return;
}
order.setStatus(OrderStatus.CLOSED);
}
// Attempt payment
// ...扣款、通知
狀態是 OPEN的訂單會先轉成 CLOSED再扣款, CLOSED 會跳過截止,直接扣款,FAILED則不由自動結算重試、只能透過手動觸發重試機制。
而這邊的重試不能造成重複扣款,因為扣款前還會用條件式 UPDATE 搶狀態,只有成功把 CLOSED/FAILED 改成 SETTLED 的執行緒才能繼續。
int claimed = orderMapper.updateStatusIfIn(orderId, OrderStatus.SETTLED,
List.of(OrderStatus.CLOSED, OrderStatus.FAILED));
if (claimed == 0) {
throw new ConflictException("訂單狀態已變更,無法結算");
}
知道「可以從哪裡繼續」後,還要處理另一個問題:「如果資料庫真的掛掉十分鐘,不能每秒重試一次。」
因此失敗後不直接放回「現在就執行」的佇列,而是延後下一次執行:
第 1 次失敗 → 1 秒後
第 2 次失敗 → 2 秒後
第 3 次失敗 → 4 秒後
第 4 次失敗 → 8 秒後
第 5 次失敗 → 16 秒後
佇列原本就用 score 表示「什麼時候執行」,所以只要把下一次執行時間往後推即可。重試次數則另外記錄,與重新排程放在同一支 Lua 腳本中,確保增加次數、移除舊任務與重新入隊不會只完成一半。
-- KEYS[1] = 佇列 KEYS[2] = 處理中
-- KEYS[3] = 重試次數 KEYS[4] = 待通知集合
-- ARGV[1] = 訂單編號 ARGV[2] = 認領時的租約(用於前段省略的租約比對)
-- ARGV[3] = 現在時間 ARGV[4..] = 第 1~5 次重試前要等的毫秒數
-- 重試上限 = 傳入的等待時間有幾個(5 個)
local maxRetries = #ARGV - 3
-- 這筆訂單的重試次數 +1,並移出處理中
local attempt = redis.call('HINCRBY', KEYS[3], ARGV[1], 1)
redis.call('ZREM', KEYS[2], ARGV[1])
-- 次數用完:清掉次數、放進待通知集合,不再排回佇列
if attempt > maxRetries then
redis.call('HDEL', KEYS[3], ARGV[1])
redis.call('SADD', KEYS[4], ARGV[1])
return 0
end
-- 還有次數:第 n 次重試等 ARGV[3 + n] 毫秒,以「現在 + 等待時間」當 score 放回佇列
local dueAt = tonumber(ARGV[3]) + tonumber(ARGV[3 + attempt])
redis.call('ZADD', KEYS[1], string.format('%.0f', dueAt), ARGV[1])
return attempt
超過次數後就不再重試,而是放進「待處理集合」,通知團主與管理員。通知失敗則重新放回集合,下一輪再通知。這樣即使結算機器在處理途中當掉,租約到期後重新執行,也會消耗同一套重試次數,不會因為換了一台機器就重新計算。