🔒

Redisによるクーポン発行の同時実行制御 (1)

に公開

問題定義

Eコマースサービスにおいて、複数のユーザーが同時に一つのクーポンにアクセスするとき、そのクーポンの在庫に限りがある場合、必ず「競合状態」が発生します。

以下の図を見ると、どのような問題が起こるかがよく理解できます。

ユーザーAとBが同時に最後のクーポンを発行しようとするとき、ロックがないシステムでは、在庫がなくなった後でも後続のユーザーBのクーポン発行が成功してしまうことがあります。さらに多くのユーザーがクーポン発行を試みれば、経済的に大きな問題に繋がるでしょう。

この問題を解決する簡単な方法はデータベースのロックですが、データベースのロックは取得と解放に時間がかかるため、多くのユーザーが利用するサービスにおいては性能上の問題となります。

Redis vs Database

ソリューション

そこで、この問題を解決するため、Redisの分散ロックを利用します。

Redisにはロックの取得・解放を行うコマンドがあります。

基本的には、このコマンドにはタイムアウト機能(有効期間)があります。ロックを取得後、指定した有効期間内に解放されなければ、そのロックは自動的に解放されます。

また、他のスレッドは、ロックが解放されるのを一定時間待機(Wait)し、その間にロックの取得を試みることができます。

今まで説明した内容を実装したコードが以下です。

public void issue(long couponId, long userId) {
    CouponRedisEntity coupon = couponCacheService.getCouponCache(couponId);
    distributeLockExecutor.execute("lock_%s".formatted(couponId), 3000, 3000, () -> {
        couponIssueRedisService.checkCouponIssueQuantity(coupon, userId);
        issueRequest(couponId, userId);
    });
}

クーポンの情報を取得し、クーポンを発行するためにロックの取得を試みます。
distributeLockExecutor.executeメソッドの4番目の引数は、ロックを取得した後に実行する処理(ラムダ式)です。

このexecuteメソッドの中を詳しく見ると、Redisでロックを取得・解放していることが確認できます。デッドロックを防ぐために、ロックの待機時間(Wait Time)と有効期間(Lease Time)の設定が重要です。

public class DistributeLockExecutor {
    private final RedissonClient redissonClient;
    private final Logger log = LoggerFactory.getLogger(this.getClass().getSimpleName());

    public void execute(String lockName, long waitMilliSecond, long leaseMilliSecond, Runnable logic) {
        RLock lock = redissonClient.getLock(lockName);
        try {
            // Redis SETNX
            boolean isLocked = lock.tryLock(waitMilliSecond, leaseMilliSecond, TimeUnit.MILLISECONDS);
            if (!isLocked) {
                throw new IllegalStateException("");
            }
            logic.run();
        } catch (InterruptedException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

まとめ

e-commerce業界で発生しがちなクーポン発行における同時実行性の問題を、Redisのコマンド(SETNX)を利用して解決してみました。データベースのロック機能を利用するよりも、Redisで実装する方が性能面でいくつかの利点があります。先ほど述べた通り、ロックの取得や解放の性能がデータベースより高速です。また、Redisは分散ロックもサポートしているため、システムの可用性を高めることもできます。

次の記事では「Luaスクリプト」を用いて、さらに性能を高めるテクニックを紹介します。

Discussion