LESSON 40分

ストーリー

「送金処理で片方だけ引き落とされて、もう片方に入金されなかった…」

障害報告を受けた高橋アーキテクトは冷静に言う。

「トランザクションが正しく設計されていないとこうなる。ACID特性を理解しているか? そしてNoSQLの世界ではBASEという別のモデルがある。どちらも理解した上で、システムに求められる整合性レベルを設計するんだ」


ACID特性

RDBのトランザクションが保証する4つの特性。

Atomicity(原子性)

トランザクション内のすべての操作が「完全に成功」か「完全に失敗」のどちらか。

-- 送金処理: 原子性の例
BEGIN;
  UPDATE accounts SET balance = balance - 10000 WHERE id = 1;  -- 引き落とし
  UPDATE accounts SET balance = balance + 10000 WHERE id = 2;  -- 入金
COMMIT;
-- どちらかが失敗したら自動的にROLLBACK
// Prismaでのトランザクション
await prisma.$transaction(async (tx) => {
  // この中のすべての操作がアトミックに実行される
  await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 10000 } },
  });

  await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 10000 } },
  });

  // ここで例外が発生すると、すべてロールバック
});

Consistency(一貫性)

トランザクション前後でデータの整合性制約が保たれる。

-- 残高が負にならない制約
ALTER TABLE accounts ADD CONSTRAINT check_balance CHECK (balance >= 0);

-- この制約により、残高不足の引き落としは自動的に失敗する
BEGIN;
  UPDATE accounts SET balance = balance - 1000000 WHERE id = 1;
  -- ERROR: new row for relation "accounts" violates check constraint
ROLLBACK;

Isolation(分離性)

同時に実行される複数のトランザクションが互いに干渉しない。

トランザクションA:         トランザクションB:
BEGIN;                     BEGIN;
READ balance → 10000       READ balance → 10000
balance - 5000             balance - 3000
WRITE balance = 5000       WRITE balance = 7000
COMMIT;                    COMMIT;

-- 分離性がないと: 最終残高が7000(Aの変更が消失)
-- 分離性があると: 正しく2000になる

Durability(永続性)

コミットされたデータは、システム障害が発生しても失われない。

COMMIT 完了

WAL(Write-Ahead Log)に書き込み

ディスクに永続化

たとえサーバーが落ちても、WALからデータを復旧可能

BASE特性

分散NoSQLシステムが採用するモデル。ACIDの対極。

Basically Available(基本的に利用可能)

システムは常にレスポンスを返す(ただし最新データとは限らない)。

Soft State(柔軟な状態)

データの状態は時間とともに変化しうる(外部入力なしでも)。

Eventually Consistent(結果整合性)

時間が経てば最終的にすべてのレプリカが同じ状態に収束する。

Write →  Node A: value = 100  ✓(即座に反映)
         Node B: value = 50   (まだ古い値)
         Node C: value = 50   (まだ古い値)

  ... 数ミリ秒~数秒後 ...

         Node A: value = 100  ✓
         Node B: value = 100  ✓(レプリケーション完了)
         Node C: value = 100  ✓(レプリケーション完了)

ACID vs BASE

特性ACIDBASE
整合性即座に一貫結果的に一貫
可用性制限あり高い
パフォーマンスオーバーヘッドあり高い
スケーラビリティ垂直スケール水平スケール
適用場面金融、在庫管理SNS、ログ、キャッシュ

結果整合性のパターン

Read-Your-Writes Consistency

自分が書き込んだデータは即座に読める。

// プロフィール更新後、更新結果を表示
async function updateAndShowProfile(userId: string, data: ProfileData) {
  // 書き込み: プライマリノードに
  await primaryDb.updateProfile(userId, data);

  // 読み取り: プライマリから読む(レプリカは遅延がある)
  return await primaryDb.getProfile(userId);
}

Monotonic Read Consistency

一度読んだ値より古い値を読むことがない。

// セッションスティッキー: 同じレプリカから読む
async function getProfile(userId: string, sessionId: string) {
  const replicaId = hash(sessionId) % replicaCount;
  return await replicas[replicaId].getProfile(userId);
}

Causal Consistency

因果関係のある操作は順序が保たれる。

ユーザーA: 投稿「質問です」        → timestamp: T1
ユーザーB: 返信「回答です」        → timestamp: T2 (T2 > T1)
ユーザーC: T2の返信は見えるがT1の質問が見えない → 因果整合性違反

TypeScriptでの整合性レベルの設計

// 整合性レベルを明示的に設計
interface DataAccessConfig {
  consistencyLevel: 'strong' | 'eventual' | 'session';
}

class UserRepository {
  // 強い整合性が必要な操作
  async getBalance(userId: string): Promise<number> {
    // プライマリDBから読む
    return await this.primaryDb.query(
      'SELECT balance FROM accounts WHERE user_id = $1',
      [userId]
    );
  }

  // 結果整合性で十分な操作
  async getFollowerCount(userId: string): Promise<number> {
    // キャッシュから読む(数秒の遅延は許容)
    const cached = await this.redis.get(`followers:${userId}`);
    if (cached) return parseInt(cached);

    const count = await this.replicaDb.query(
      'SELECT COUNT(*) FROM follows WHERE following_id = $1',
      [userId]
    );
    await this.redis.set(`followers:${userId}`, count.toString(), 'EX', 30);
    return count;
  }
}

整合性レベルの設計判断

データ種別必要な整合性理由
残高・在庫強い整合性金銭的損失、売り越し防止
ユーザープロフィールRead-Your-Writes自分の更新は即座に見えるべき
タイムライン結果整合性数秒の遅延は許容
いいね数結果整合性近似値で十分
セッションセッション整合性同一セッション内で一貫

まとめ

ポイント内容
ACID原子性・一貫性・分離性・永続性
BASE基本的可用性・柔軟な状態・結果整合性
選択基準データの重要度と許容できる遅延で判断
結果整合性Read-Your-Writes, Monotonic Read, Causal
設計の鍵データ種別ごとに適切な整合性レベルを選択

理解度チェックリスト

  • ACID の4特性をそれぞれ説明できる
  • BASE モデルの特徴を説明できる
  • 結果整合性のパターン(Read-Your-Writes等)を理解している
  • データ種別に応じた整合性レベルを判断できる

次のステップ

次のレッスンではロック戦略と分離レベルを深掘りする。楽観的ロック・悲観的ロック、そして4つの分離レベルを理解しよう。


推定読了時間: 40分