🎄 科学と神々株式会社 アドベントカレンダー 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を選んだ理由
-
トランザクション一貫性
- ユーザー作成とライセンス発行をACIDで保証
-
実装の簡素化
- 分散トランザクション不要
- データ同期機構不要
-
パフォーマンス
- ネットワークオーバーヘッドなし
- SQLiteの高速性を活用
-
小規模システムに適切
- 数百万ユーザー規模まで対応可能
- 過度な複雑化を避ける
テーブル所有権の定義
-- 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.