0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 5: マイクロサービスのデータ管理

Last updated at Posted at 2025-12-04

🎄 科学と神々株式会社 アドベントカレンダー 2025

Hybrid License System Day 5: マイクロサービスのデータ管理

マイクロサービス基礎編 (4/4)


📖 はじめに

Day 5では、マイクロサービスのデータ管理を学びます。Shared Databaseパターン、トランザクション管理、データ一貫性の保証方法を理解しましょう。


🗄️ データ管理パターン

Database per Service vs Shared Database

Pattern 1: Database per Service(理想形)

各サービスが独立したデータベースを持つパターンです。

┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ Auth Service │  │Admin Service │  │Other Service │
└───────┬──────┘  └───────┬──────┘  └───────┬──────┘
        │                 │                 │
   ┌────▼────┐       ┌────▼────┐       ┌────▼────┐
   │  Auth   │       │  Admin  │       │  Other  │
   │   DB    │       │   DB    │       │   DB    │
   └─────────┘       └─────────┘       └─────────┘

メリット:

  • ✅ 完全な独立性
  • ✅ 技術スタックの自由度(PostgreSQL, MongoDB, Redisなど)
  • ✅ スケーリングの柔軟性

デメリット:

  • ❌ トランザクション管理が複雑
  • ❌ データ同期の必要性
  • ❌ JOIN不可(APIで結合)

Pattern 2: Shared Database(Hybrid実装)

複数サービスが1つのデータベースを共有するパターンです。

┌──────────────┐  ┌──────────────┐
│ Auth Service │  │Admin Service │
└───────┬──────┘  └───────┬──────┘
        │                 │
        └────────┬────────┘
                 │
           ┌─────▼─────┐
           │  Shared   │
           │  SQLite   │
           │  Database │
           └───────────┘

メリット:

  • ✅ トランザクション一貫性
  • ✅ シンプルな実装
  • ✅ JOINが可能

デメリット:

  • ❌ サービス間の結合度上昇
  • ❌ スキーマ変更の影響範囲拡大
  • ❌ スケーリングの制約

📐 Hybrid実装の選択理由

Shared Databaseを選んだ理由

  1. トランザクション一貫性

    • ユーザー作成とライセンス発行をACIDで保証
  2. 実装の簡素化

    • 分散トランザクション不要
    • データ同期機構不要
  3. パフォーマンス

    • ネットワークオーバーヘッドなし
    • SQLiteの高速性を活用
  4. 小規模システムに適切

    • 数百万ユーザー規模まで対応可能
    • 過度な複雑化を避ける

テーブル所有権の定義

-- Auth Serviceが所有(読み書き)
CREATE TABLE users (
  user_id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  plan TEXT NOT NULL,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);

CREATE TABLE licenses (
  license_id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  client_id TEXT UNIQUE NOT NULL,
  status TEXT NOT NULL,
  created_at TEXT NOT NULL,
  expires_at TEXT NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(user_id)
);

CREATE TABLE subscriptions (
  subscription_id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL,
  plan TEXT NOT NULL,
  status TEXT NOT NULL,
  started_at TEXT NOT NULL,
  renews_at TEXT NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- Admin Serviceが所有(読み書き)
CREATE TABLE audit_logs (
  log_id TEXT PRIMARY KEY,
  user_id TEXT,
  action TEXT NOT NULL,
  details TEXT,
  ip_address TEXT,
  timestamp TEXT NOT NULL
);

-- 参照関係のルール
-- Auth Service: users, licenses, subscriptions への読み書き可
-- Admin Service: users, licenses への読み取りのみ、audit_logs への読み書き可

🔄 トランザクション管理

ACID特性の保証

SQLiteはACIDトランザクションをサポートしています。

// auth-service/src/dbService.js
class DBService {
  /**
   * トランザクション内でライセンス作成
   */
  createLicenseTransaction(userId, clientId, plan) {
    // トランザクション開始
    const transaction = this.db.transaction((userId, clientId, plan) => {
      // 1. ユーザー存在確認
      const user = this.db.prepare('SELECT * FROM users WHERE user_id = ?').get(userId);
      if (!user) {
        throw new Error('User not found');
      }

      // 2. 既存ライセンスチェック
      const existing = this.db.prepare('SELECT * FROM licenses WHERE client_id = ?').get(clientId);
      if (existing) {
        throw new Error('License already exists');
      }

      // 3. ライセンス作成
      const licenseId = `lic-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
      const now = new Date().toISOString();
      const expiresAt = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString();

      this.db.prepare(`
        INSERT INTO licenses (license_id, user_id, client_id, status, created_at, expires_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `).run(licenseId, userId, clientId, 'active', now, expiresAt);

      // 4. 監査ログ記録
      this.db.prepare(`
        INSERT INTO audit_logs (log_id, user_id, action, details, timestamp)
        VALUES (?, ?, ?, ?, ?)
      `).run(
        `log-${Date.now()}`,
        userId,
        'LICENSE_CREATED',
        JSON.stringify({ licenseId, clientId }),
        now
      );

      return { licenseId, userId, clientId, status: 'active', expiresAt };
    });

    // トランザクション実行
    return transaction(userId, clientId, plan);
  }
}

ロールバック

try {
  const result = dbService.createLicenseTransaction(userId, clientId, plan);
  console.log('License created successfully:', result);
} catch (error) {
  // トランザクション全体がロールバックされる
  console.error('Transaction failed, rolled back:', error.message);
  throw error;
}

📊 データ一貫性の保証

Read-After-Write一貫性

// ライセンス作成直後に読み取り
async function activateAndValidate(email, password, clientId) {
  // 1. ライセンス作成
  const license = await authService.activate(email, password, clientId);

  // 2. 即座に検証(同じデータベースなので一貫性保証)
  const validation = await authService.validate(license.token);

  if (!validation.valid) {
    throw new Error('Created license validation failed');
  }

  return license;
}

楽観的ロック

// version カラムを使った楽観的ロック
class LicenseService {
  updateLicenseStatus(licenseId, newStatus, expectedVersion) {
    const result = this.db.prepare(`
      UPDATE licenses
      SET status = ?, version = version + 1, updated_at = ?
      WHERE license_id = ? AND version = ?
    `).run(newStatus, new Date().toISOString(), licenseId, expectedVersion);

    if (result.changes === 0) {
      throw new Error('License was modified by another process');
    }

    return result;
  }
}

🔍 クエリパターン

Auth Serviceのクエリ

// シンプルなクエリ(高速)
class AuthService {
  getUserByEmail(email) {
    return this.db.prepare('SELECT * FROM users WHERE email = ?').get(email);
  }

  getLicenseByClientId(clientId) {
    return this.db.prepare('SELECT * FROM licenses WHERE client_id = ?').get(clientId);
  }

  // インデックスを活用
  getActiveLicensesByUserId(userId) {
    return this.db.prepare(`
      SELECT * FROM licenses
      WHERE user_id = ? AND status = 'active'
      ORDER BY created_at DESC
    `).all(userId);
  }
}

Admin Serviceのクエリ

// 集計クエリ(複雑)
class AdminService {
  getUserStatistics() {
    return this.db.prepare(`
      SELECT
        plan,
        COUNT(*) as user_count,
        COUNT(CASE WHEN created_at > datetime('now', '-30 days') THEN 1 END) as new_users_30d
      FROM users
      GROUP BY plan
    `).all();
  }

  getLicenseStatistics() {
    return this.db.prepare(`
      SELECT
        status,
        COUNT(*) as count,
        MIN(created_at) as oldest,
        MAX(created_at) as newest
      FROM licenses
      GROUP BY status
    `).all();
  }

  // JOIN クエリ(Shared Databaseの利点)
  getUserWithLicenses(userId) {
    return this.db.prepare(`
      SELECT
        u.user_id,
        u.email,
        u.plan,
        l.license_id,
        l.client_id,
        l.status,
        l.expires_at
      FROM users u
      LEFT JOIN licenses l ON u.user_id = l.user_id
      WHERE u.user_id = ?
      ORDER BY l.created_at DESC
    `).all(userId);
  }
}

🚀 パフォーマンス最適化

インデックス設計

-- usersテーブル
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_plan ON users(plan);
CREATE INDEX idx_users_created_at ON users(created_at);

-- licensesテーブル
CREATE INDEX idx_licenses_user_id ON licenses(user_id);
CREATE INDEX idx_licenses_client_id ON licenses(client_id);
CREATE INDEX idx_licenses_status ON licenses(status);
CREATE INDEX idx_licenses_expires_at ON licenses(expires_at);

-- audit_logsテーブル
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_timestamp ON audit_logs(timestamp);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);

プリペアドステートメント

class DBService {
  constructor(dbPath) {
    this.db = new Database(dbPath);

    // プリペアドステートメントをキャッシュ
    this.statements = {
      getUserByEmail: this.db.prepare('SELECT * FROM users WHERE email = ?'),
      getLicenseByClientId: this.db.prepare('SELECT * FROM licenses WHERE client_id = ?'),
      insertLicense: this.db.prepare(`
        INSERT INTO licenses (license_id, user_id, client_id, status, created_at, expires_at)
        VALUES (?, ?, ?, ?, ?, ?)
      `)
    };
  }

  getUserByEmail(email) {
    return this.statements.getUserByEmail.get(email);
  }

  // ... 他のメソッド
}

バッチ処理

// 複数ライセンスの一括更新
class LicenseService {
  batchUpdateStatuses(updates) {
    const transaction = this.db.transaction((updates) => {
      const stmt = this.db.prepare(`
        UPDATE licenses SET status = ?, updated_at = ? WHERE license_id = ?
      `);

      for (const { licenseId, status } of updates) {
        stmt.run(status, new Date().toISOString(), licenseId);
      }
    });

    transaction(updates);
  }
}

🔄 データ移行とバックアップ

バックアップ戦略

# 定期的なバックアップ(cronで実行)
#!/bin/bash
BACKUP_DIR="/backup/licenses"
DB_PATH="/app/data/licenses.db"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# SQLiteデータベースをコピー
sqlite3 $DB_PATH ".backup ${BACKUP_DIR}/licenses_${TIMESTAMP}.db"

# 古いバックアップを削除(30日以上前)
find $BACKUP_DIR -name "licenses_*.db" -mtime +30 -delete

データマイグレーション

// スキーマバージョン管理
class MigrationService {
  async migrate() {
    const currentVersion = this.getCurrentVersion();

    const migrations = [
      { version: 1, up: this.migration_001 },
      { version: 2, up: this.migration_002 },
      { version: 3, up: this.migration_003 }
    ];

    for (const migration of migrations) {
      if (currentVersion < migration.version) {
        console.log(`Running migration ${migration.version}`);
        await migration.up.call(this);
        this.setCurrentVersion(migration.version);
      }
    }
  }

  migration_001() {
    // 初期スキーマ作成
    this.db.exec(`CREATE TABLE users (...)`);
    this.db.exec(`CREATE TABLE licenses (...)`);
  }

  migration_002() {
    // audit_logsテーブル追加
    this.db.exec(`CREATE TABLE audit_logs (...)`);
  }

  migration_003() {
    // インデックス追加
    this.db.exec(`CREATE INDEX idx_licenses_status ON licenses(status)`);
  }
}

⚖️ Shared Database vs Database per Service 比較

Hybrid実装での判断基準

要件 Shared DB Database per Service
トランザクション一貫性 ✅ ACID保証 ❌ 分散トランザクション必要
実装の複雑さ ✅ シンプル ❌ 複雑
スケーリング ❌ 制約あり ✅ 柔軟
データ独立性 ❌ 結合度高い ✅ 完全独立
パフォーマンス ✅ JOIN可能 ❌ API経由結合

将来的な分離計画

Phase 1(現在): Shared Database
  - SQLite 単一ファイル
  - すべてのサービスが共有

Phase 2(中期): Logical Separation
  - PostgreSQLに移行
  - スキーマで論理分離
  - Auth: schema "auth"
  - Admin: schema "admin"

Phase 3(長期): Physical Separation
  - 完全に独立したデータベース
  - Auth: PostgreSQL(高可用性)
  - Admin: PostgreSQL(分析最適化)
  - イベントソーシングで同期

🎯 次のステップ

Day 6では、API Gatewayの役割と責務について学びます。ルーティング、レート制限、セキュリティヘッダー、ヘルスチェックの実装を理解しましょう。


🔗 関連リンク


次回予告: Day 6では、API GatewayのExpress.js実装と、セキュリティミドルウェアの詳細を解説します!


Copyright © 2025 Gods & Golem, Inc. All rights reserved.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?