什麼是 ACID?
ACID 是資料庫交易(Transaction)的四個基本特性,確保資料操作的正確性與可靠性:
| 特性 | 中文 | 說明 |
|---|---|---|
| Atomicity | 原子性 | 交易內所有操作全部成功或全部失敗,不允許部分完成 |
| Consistency | 一致性 | 交易完成後,資料庫必須遵守所有原先定義的規則與約束 Ex.錢包餘額不可為負 |
| Isolation | 隔離性 | 多個並行交易的執行結果,彼此不能互相干擾 |
| Durability | 持久性 | 已提交的交易結果,即使系統崩潰也必須永久保存 |
什麼是 Atomicity?為什麼重要?
Atomicity 指的是:一個 transaction 內的所有操作,要馬全部成功,要馬全部當作沒發生過。
白話來說:資料處理不能做到一半失敗,然後資料庫留下一半髒資料。
違反 Atomicity 的真實案例
舉一個我在自己專案犯的錯誤
想像一筆玩家儲值交易因金流服務異常被標記為「異常交易」
系統修復後執行結案流程:
- 把異常交易狀態改成「已解決」
- 把儲值金額加到玩家的遊戲錢包
- 更新該玩家的累積儲值總額
- 記錄這筆操作到日誌(Audit Log)
如果步驟 2 失敗了,但步驟 1 已經存進資料庫
結果就是:異常交易顯示已解決,但錢根本沒進玩家錢包。玩家儲值不到帳,系統卻顯示已完成,這就是違反 Atomicity (原子性)。
以程式碼來看
async function resolveAbnormalTopUp(orderId: string) {
const order = await db.topUpOrder.findUnique({
where: { id: orderId },
});
if (!order) {
throw new Error("找不到儲值訂單");
}
// 1. 先把異常交易狀態改成「已解決」
await db.topUpOrder.update({
where: { id: orderId },
data: {
status: "RESOLVED",
resolvedAt: new Date(),
},
});
// 2. 把儲值金額加到玩家錢包
// 假設這一步因為 DB timeout、資料鎖定、網路問題而失敗
await db.wallet.update({
where: { playerId: order.playerId },
data: {
balance: {
increment: order.amount,
},
},
});
// 3. 更新玩家累積儲值總額
await db.player.update({
where: { id: order.playerId },
data: {
totalTopUpAmount: {
increment: order.amount,
},
},
});
// 4. 記錄 Audit Log
await db.auditLog.create({
data: {
action: "RESOLVE_ABNORMAL_TOP_UP",
playerId: order.playerId,
orderId: order.id,
amount: order.amount,
message: "異常儲值訂單已結案並補發金額",
},
});
}
這段程式碼的問題是:每一步都直接寫入資料庫,但沒有包在同一個 transaction 裡。
只要某個步驟失敗了,資料就會變成不一致的狀態
正確做法:用 Transaction 包起來
因為這整個流程本來應該被視為 「同一筆交易」 :
更新訂單狀態 + 補發錢包金額 + 更新累積儲值 + 寫入日誌
typescript
要嘛全部成功,要嘛全部失敗,不能只有第一步成功,後面失敗。
以程式碼來看
async function resolveAbnormalTopUp(orderId: string) {
await db.$transaction(async (tx) => {
const order = await tx.topUpOrder.findUnique({
where: { id: orderId },
});
if (!order) {
throw new Error("找不到儲值訂單");
}
// 1. 更新異常交易狀態
await tx.topUpOrder.update({
where: { id: orderId },
data: {
status: "RESOLVED",
resolvedAt: new Date(),
},
});
// 2. 增加玩家錢包餘額
await tx.wallet.update({
where: { playerId: order.playerId },
data: {
balance: {
increment: order.amount,
},
},
});
// 3. 更新玩家累積儲值總額
await tx.player.update({
where: { id: order.playerId },
data: {
totalTopUpAmount: {
increment: order.amount,
},
},
});
// 4. 寫入 Audit Log
await tx.auditLog.create({
data: {
action: "RESOLVE_ABNORMAL_TOP_UP",
playerId: order.playerId,
orderId: order.id,
amount: order.amount,
message: "異常儲值訂單已結案並補發金額",
},
});
});
}
使用 transaction 之後,只要其中任何一步失敗,例如錢包更新失敗:
await tx.wallet.update(...)整個 transaction 就會 rollback。
也就是前面已經執行過的步驟都會被還原,最後資料庫會保持在原本狀態
這樣就符合 Atomicity:這筆流程,要嘛完整成功,要嘛完全不發生。
Top comments (0)