Redis分散ロックの実装:PmHubにおけるフローステータス管理の信頼性向上

分散システムにおけるデータ整合性の確保手法

マイクロサービス環境では、複数ノード間でのリソース競合を防止するための分散ロック機構が不可欠です。本稿では、ワークフローマネジメントシステムで発生するステータス更新競合を解消するためのRedisベースの実装手法を解説します。

単一サーバー環境のロック限界

JavaのsynchronizedやReentrantLockはJVM内でのみ有効です。複数インスタンスが稼働する環境では、各インスタンスが独立したロックオブジェクトを保持するため、データベース操作の排他制御が成立しません。

// 単一サーバー環境での不適切な実装例
public class UnsafeService {
    private final Object lock = new Object();
    
    public void updateStatus(StatusRequest request) {
        synchronized (lock) {
            // データベース更新処理
        }
    }
}

この実装では、各インスタンスが独立してロックを取得可能で、実質的な排他制御が機能しません。

分散ロックの設計要件

  1. 排他性保証:同時刻に単一ノードのみがロックを保持可能
  2. 自動解放:処理異常時にロックが永続化しないようタイムアウト機構を必須
  3. 自動延長:長期処理時にWatchDogメカニズムでタイムアウトを動的に延長
  4. 所有者識別:ロック解放時の誤削除防止のため所有者識別子を付与

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%を超えた場合にスケールアウトを検討する方針としています。

タグ: Redisson distributed-lock spring-aop redis concurrency-control

6月20日 20:46 投稿