DEV Community

Cover image for [🗄️DataBase] ACID - Atomicity
Mercy
Mercy

Posted on

[🗄️DataBase] ACID - Atomicity

什麼是 ACID?

ACID 是資料庫交易(Transaction)的四個基本特性,確保資料操作的正確性與可靠性:

特性 中文 說明
Atomicity 原子性 交易內所有操作全部成功或全部失敗,不允許部分完成
Consistency 一致性 交易完成後,資料庫必須遵守所有原先定義的規則與約束 Ex.錢包餘額不可為負
Isolation 隔離性 多個並行交易的執行結果,彼此不能互相干擾
Durability 持久性 已提交的交易結果,即使系統崩潰也必須永久保存

什麼是 Atomicity?為什麼重要?

Atomicity 指的是:一個 transaction 內的所有操作,要馬全部成功,要馬全部當作沒發生過

白話來說:資料處理不能做到一半失敗,然後資料庫留下一半髒資料


違反 Atomicity 的真實案例

舉一個我在自己專案犯的錯誤

想像一筆玩家儲值交易因金流服務異常被標記為「異常交易」

系統修復後執行結案流程:

  1. 把異常交易狀態改成「已解決」
  2. 把儲值金額加到玩家的遊戲錢包
  3. 更新該玩家的累積儲值總額
  4. 記錄這筆操作到日誌(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: "異常儲值訂單已結案並補發金額",
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

這段程式碼的問題是:每一步都直接寫入資料庫,但沒有包在同一個 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: "異常儲值訂單已結案並補發金額",
      },
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

使用 transaction 之後,只要其中任何一步失敗,例如錢包更新失敗:

await tx.wallet.update(...)

整個 transaction 就會 rollback。
也就是前面已經執行過的步驟都會被還原,最後資料庫會保持在原本狀態
這樣就符合 Atomicity這筆流程,要嘛完整成功,要嘛完全不發生。

Top comments (0)