分散システムにおけるデータ整合性の確保手法
マイクロサービス環境では、複数ノード間でのリソース競合を防止するための分散ロック機構が不可欠です。本稿では、ワークフローマネジメントシステムで発生するステータス更新競合を解消するためのRedisベースの実装手法を解説します。
単一サーバー環境のロック限界
JavaのsynchronizedやReentrantLockはJVM内でのみ有効です。複数インスタンスが稼働する環境では、各インスタンスが独立したロックオブジェクトを保持するため、データベース操作の排他制御が成立しません。
// 単一サーバー環境での不適切な実装例
public class UnsafeService {
private final Object lock = new Object();
public void updateStatus(StatusRequest request) {
synchronized (lock) {
// データベース更新処理
}
}
}
この実装では、各インスタンスが独立してロックを取得可能で、実質的な排他制御が機能しません。
分散ロックの設計要件
- 排他性保証:同時刻に単一ノードのみがロックを保持可能
- 自動解放:処理異常時にロックが永続化しないようタイムアウト機構を必須
- 自動延長:長期処理時にWatchDogメカニズムでタイムアウトを動的に延長
- 所有者識別:ロック解放時の誤削除防止のため所有者識別子を付与
Redissonを活用した実装
Redisの高性能とRedissonの高機能を組み合わせた実装が実用的です。以下に主要コンポーネントの再設計例を示します。
設定クラスの再構築
@Configuration
public class LockingInfrastructure {
@Value("${redis.endpoint}")
private String endpoint;
@Bean
public RedissonClient distributedLockClient() {
SingleServerConfig config = new SingleServerConfig()
.setAddress("redis://" + endpoint)
.setTimeout(3000);
Config redissonConfig = new Config()
.setCodec(JsonJacksonCodec.INSTANCE)
.useSingleServer(config);
return Redisson.create(redissonConfig);
}
}
ロック管理インターフェース
public interface LockManager {
ResourceLock acquire(String key, Duration waitTime);
void release(ResourceLock lock);
class ResourceLock implements AutoCloseable {
private final RLock internalLock;
ResourceLock(RLock lock) {
this.internalLock = lock;
}
@Override
public void close() {
internalLock.unlock();
}
}
}
アスペクト指向の実装
@Aspect
@Component
public class LockingAspect {
private final LockManager locker;
private final ExpressionEvaluator evaluator = new ExpressionEvaluator();
@Around("@annotation(lockConfig)")
public Object manageLock(ProceedingJoinPoint joinPoint, LockConfig lockConfig)
throws Throwable {
String lockKey = buildKey(lockConfig.key(), joinPoint);
try (ResourceLock lock = locker.acquire(
lockKey,
Duration.ofSeconds(lockConfig.waitTime())
)) {
return joinPoint.proceed();
} catch (LockAcquireException e) {
throw new ConflictException("同時実行制御エラー");
}
}
private String buildKey(String expression, ProceedingJoinPoint joinPoint) {
if (!expression.contains("#")) return expression;
return evaluator.resolve(expression, joinPoint);
}
}
実際の適用事例
ワークフローステータス更新処理では、以下の注釈をコントローラーメソッドに付与します。
@LockConfig(
key = "#request.processId",
waitTime = 5,
lockDuration = 20
)
@PutMapping("/process-status")
public ResponseEntity updateProcessStatus(@RequestBody StatusUpdateRequest request) {
// ステータス更新ロジック
}
この実装により、同一プロセスIDに対する並列更新を防止し、データ不整合を回避します。ロックキーにプロセスIDを組み込むことで、異なるワークフロー間の干渉を最小限に抑えています。
実装のポイント
- ロックタイムアウト値は業務処理時間の1.5倍を推奨
- スループット向上のため、単一Redisインスタンスではなくクラスタ構成を採用
- スレッドリーク防止のため、必ずtry-with-resources構文でロックを解放
- 監視用にロック取得/解放のメトリクスをDatadogに送信
実運用では、ロック競合率が5%を超えた場合にスケールアウトを検討する方針としています。