photo by John Cameron
剛好昨天看了小Lin老師的影片,提到委內瑞拉這個國家,天天都在排隊
加油排隊、去超市也排隊
排隊這件事,其實跟系統處理工作的順序一樣
順序一亂,待季就大條了,這時候使用Queue來管理執行順序,是一個很好的方法
什麼是 Queue
Queue 是一種 先進先出 (First-In,First-Out,FIFO) 的資料結構
Queue跟Stack一樣,定義了使用者只能用那些操作
換來一致性的時間複雜度
Queue的時間複雜度
| 操作 | Queue | 時間複雜度 |
|---|---|---|
| push | 插入資料到最後位置 | O(1) |
| pop | 刪除最前面的資料 | O(1) |
| front | 取得最前面的資料 | O(1) |
使用Queue最佳場景
假設現在有三個Job依序執行,而執行中很可能因為系統當機導致需要重做
這時候應該把Job放在哪種資料結構中:
A:List搭配index
B:Queue
看起來A、B都可以,實際上會造成很大的落差
情境A List搭配index
list = [A, B, C]
i = load_index() // 持久化的進度
while i < list.length:
job = list[i]
do_side_effect(job) // 例如:寄信 / 扣款 / 發送檔案
// 這裡可能成功、也可能一半成功、也可能成功但你還沒來得及記錄
i = i + 1
save_index(i) // 持久化進度
crash可能發生在:
假設執行到job B,do_side_effect(job)執行了,但還沒save_index(i),造成
- 外部世界:job B已經寄信/扣款等行為
- 你的系統:i還停留在執行完job A的狀態
- 重啟後:B會再做一次,例如再扣一次錢
save_index(i)成功了,但do_side_effect(job)實際上沒完成,例如API timeout,DB transaction rollback,結果:
- 你的系統:以為job B已經做完了
- 重啟後:B不會再做,漏執行
使用Queue
queue = [A, B, C]
while queue is not empty:
job = queue.front() // 先看,不移除
do_side_effect(job)
queue.dequeue() // 成功後才移除
crash在dequeue()之前
- queue裡仍然有job B
- 重啟後:B會再做一次,例如再扣一次錢
crash在dequeue()之後
- queue裡沒有job B
- 重啟後:B不會再做,因為已經完成
簡單總結:
- List + index 把「改變到下一個狀態」這件事拆分成兩個步驟,造成crash時可能的風險
- Queue 把「改變到下一個狀態」這件事包裝成一個步驟
在實際系統中,Queue通常會搭配:
- ack / commit 機制:告訴系統這個job已經做完
- at-least-once 處理策略:不要漏做,寧願多做,漏做通常比重做更嚴重
- idempotent:同一個工作被重複執行多次,系統最終狀態仍然正確,不會有副作用