ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

集中アクセスに対応するサイトの仕組みについて考えてみた

こんにちは、プロダクトエンジニアリング部のちょうです。ずっと家にいまして時間の流れが気づきにくいと思いませんか。知らないうちに8月も何日しか残らないし、2021年も3分の2までが終わっています。家でテレワークしながら、何かおもしろいものないかなと考えたら、ふっと最近使っていたワクチン接種サイトを思い出しました。

ワクチン接種などのサイトは大量なユーザーから集中的なアクセスを予想できます。いくらサーバーを増やしてもさばけないと思われます。そしたら

  1. 接続数を制限
  2. アクセス時間帯を分ける
  3. キュー

などの施策が必要となります。アクセス時間帯を分けるのは利用するユーザーのIDなどをベースにアクセスできるタイミングを分散して集中アクセスを減らすアプローチです。そして減らした集中アクセスを予測してサーバーを増やすだけで対応できる可能性があります。すごくシンプルな考え方です。

そもそも負荷によって自動的にサーバーを増やして対応してもいいのではと思ってもかもしれませんが、ワクチン接種いわゆるチケット予約サイトはサーバーよりデータベースなどが先にボトルネックになってしまいます。複数枠、例えばワクチン接種予約の時間帯、を用意しても一つのレコードにロックをかけると、処理は一つずつになります(アクセスが集中するとき楽観ロックより悲観ロックがいいというデータがある)。案件によって、予約して裏側で非同期処理で結果を通知する仕組みもありますが、やはり時間内で結果を知りたいのが多いです。そうすると、リクエストをさばけるように事前に処理能力を計算し、サイトに入れるユーザーを制限するわけです。

制限といったら、API制限というのがあります。一般的にAPI制限はtoken bucketなどのアルゴリズムを利用し、一定時間内のアクセス数を制限します。これらのアルゴリズムは基本一台のサーバーなら簡単ですが、複数サーバーになると、Redisに計測値を共有して特殊のスクリプト(token bucketアルゴリズムにすくなくとも時刻と残るtoken数などのデータがある、Redisで安全に複数データを変更する方法がないため)で計算する方法があるらしいが、私が以前、1000回/60秒で2台でしたら500回/60秒を一台ずつ、サーバー数増減したら各サーバーの回数を自動的に調整できる仕組みを作っていました。ただ、API制限は基本チケット予約サイトに使えません。制限を超えたら、エラーになるだけです。ユーザーにとっては、「ただいまアクセスが集中しています」みたいの表示になり、リロードを試みるしかないです。

残りはキュー一択になります。自分の知る限り、ticket master、オリンピックチケット予約サイトなどに入ると、あなたがいま何番目のが表示され、自分の番になるまで待つだけでいいです。とても親切のように見えます。中身は何なのかはおそらく作った人しかわからないですが、今日は自分の理解でどうすれば同じ仕組みを作れるかを説明してみたいと思います。

処理能力内でしたらサイトに入る、処理能力を超えたら待つという仕組みは実際並列処理のSemaphoreととても似ています。Semaphoreに決まった数の許可(permit)があり、許可を取ろう(acquire)とするとき、まだ許可が残っているなら即時成功、残っていないなら待つ状態になります。一方、許可を取得したスレッドが許可を戻す(release)するとき、待っているスレッドがあればその許可をもらって処理を続けます。ここまで理解できるひとは「同時に取ろうとするスレッドがあれば誰がその許可をもらう?」という疑問を持っているかもしれません。ここで最初に待っているスレッドがもらうなら公平(fair)なSemaphoreといい、同時に取ろうとするスレッドが取れる場合は不公平(unfair)なSemaphoreといいです。現実でいうと先頭に割り込みとなります。ではSemaphoreとチケット予約サイトを比較してみましょう。

チケット予約サイト Semaphore
予約者 スレッド
サイトに入る 許可を取る
予約完了 許可を戻す
待ち画面 スレッドが待つ
予約画面に入る スレッドが起きる

厳密でいうと、Semaphore側スレッドが待ったり起きたりすることができません。OSのスケジューラーでスレッドを実行するかしないかだけで、ハイレベルのプログラミング言語側はもちろんスケジューラーにアクセスできないので、排他制御のConditionVariableと利用することがあります。

チケット予約サイトとSemaphoreは似ているとわかったところで、Semaphoreはどうやって作ったかというと、許可を表すpermitsと待ち行列queueです。もちろん実際は複数スレッドからアクセスすることもありとても複雑です。そして待ち行列はSemaphoreの内部にあり、そのままチケット予約サイトに使えません。なぜならユーザーリクエストにあたる接続やスレッドをそのまま待つ状態にすることができないからです。画面上に待ち状態を表したいなら、レスポンスが必要となります。そうすると、試しに許可を取る(tryAcquire)を利用して、失敗したら現在アクセスが集中していますと出せばいいのではというと少し違いです。そもそも失敗したら待ち状態になっていません。結局のところ、Webサイト用のSemaphoreをつくらないといけないのです。

では、Webサイト専用のSemaphoreはどんな要件があるのですか。とりあえず先頭に割り込みのは嫌なので、それ以外チケット予約サイトならではの要件があると思います。

  • 公平fair
  • レスポンスは一定時間内で返す

そして、途中で許可数を変えられるかについて、設計によって変えられないと思ってください。事前にパフォーマンステストを行い、システムの限界を知った上でSemaphoreを設定しましょう。もう一つ、ユーザーにどうやって通知することです。一定の間隔で状態を確認するとサイトに攻撃するようなことになります。できればサイトから通知するほうがいいでしょう。サイトとユーザーの接続について、NIO(Nonblocking IO)を利用することで、すくないスレッドで接続を管理することができます。

では私は設計したアプローチはこのようなものです。

f:id:unifa_tech:20210827152807p:plain

実は私が以前Semaphoreを作ったことがあります。JSR236つまりJava配列処理ライブラリにAQS(AbstractQueuedSynchronizer)というとても複雑なスレッドの待ち行列をベースに、ロック、ReadWriteロック、Semaphoreなどを作られていますので、私が既存のコードを理解しつつ、自分のバージョンを作ってみました。ただAQSはスレッドをベースにするクラスで、Webサイトには使えません。

一般的にSemaphoreは許可を取ろうとするとき先に許可(permits)を変更してみます。公平fairの場合は、先にキューを見てみます。誰が待っているなら、キューに入ります。ここにキューに入ったら突然許可があったのケースがあり、許可を持っているスレッドが許可を戻すとき待っている人がいるかどうかをチェックするタイミングでまだキューに入ってなかったり、入ったら待つ状態に変えたりいくつかレアだが、考慮しないといけないシーンがあります。

チケット予約サイトにとっては、時間的に分散して来るより集中アクセスが怖いです。集中アクセスの場合、許可がすくないため、ほとんどの人はキューに入ると予測できます。この特徴を利用して、まず皆さん全員列(queue)に並んで、案内人(worker)が順番で受付します。列に並ぶとき、整理券を配ったり(待ち番号)、予測を出したり、案内するとき、許可をあげたりします。これで、ユーザーが列に並んだら待ち状態になり、サイト側で一瞬でレスポンスが来て画面を表示できます。案内人は1秒の間隔で、許可数をチェックして、先頭から許可を与えます。もうひとつのポイントは許可をもらった人がチケットを予約し終わったら、直接先頭の人に許可を渡します。

この案のメリットは全員に一瞬でレスポンスできます。処理が終わった人にも役割があり、案内人と同時に行動することができます。デメリットは先頭の人は最大1秒をまたないといけないです。

Semaphoreと比較すると、案内人という新しいものが出ています。案内人が存在する意味がいくつかあります。一つ目は全員列に入ったあと最初に許可を与える人、2つ目は許可を与えた人に処理が長すぎではないかのチェックができるのです。場合によって、処理が止まってる人の許可を無効化して、新しい許可を発行します。処理が終わった人と先頭にいる人のバトンタッチはもともとSemaphoreにあり、特に新しいものではありません。

実際の予約サイト用Semaphoreコード(Java)。

static class ReservationSemaphore implements Runnable {
    private final ConcurrentLinkedQueue<UserRequest> queue = new ConcurrentLinkedQueue<>();
    private int permits; // only accessed by worker
    private final AtomicInteger reservedPermits = new AtomicInteger(0);

    ReservationSemaphore(int permits) {
        this.permits = permits;
    }

    void acquire(UserRequest request) {
        queue.offer(request);
    }

    void release() {
        UserRequest next = queue.poll();
        if (next != null) {
            next.signal();
        } else {
            reservedPermits.incrementAndGet();
        }
    }

    @Override
    public void run() {
        while (availablePermits() > 0) {
            UserRequest next = queue.poll();
            if (next == null) break;
            permits--;
            next.signal();
        }
    }

    private int availablePermits() {
        if (permits == 0) {
            int rp;
            while ((rp = reservedPermits.get()) > 0) {
                // move permits
                if (reservedPermits.compareAndSet(rp, 0)) {
                    logger.debug("move reserved permits {} to available permits", rp);
                    permits += rp;
                }
            }
        }
        return permits;
    }
}

プログラミング言語には複数スレッドからの同時アクセスができればオーケーです。プログラミング言語より後ろの考え方に注目してください。

許可を取る(acquire)はqueueに入るだけです。許可を返す(releaseは)処理が終わったら人から先頭にいる人へのバトンタッチです。案内人(run)は現在の許可を確認して先頭の人に許可を与えます。

すこし複雑な部分は現在可能な許可(availablePermits)のコードです。案内人の手にある許可数(permits)と処理が終わったら戻す許可数を分ける理由は案内人と処理が終わった人できるだけ共有する部分を減らしたいです。処理が終わった人から次に先頭を渡したいですが、先頭に誰もいないとき許可を案内人に戻します。増減する変数(許可数)にABA問題があるかもしれません。ABA問題はわかりづらいのでここで割愛します。コードには案内人の許可数が減るだけ、処理が終わったら人から増やすだけもしかしてABA問題がないと思いますが、案内人の手に許可を持たせて、隣にトレー(reversedPermits)を用意して、処理が終わった人の許可をそこに置いてくださいにすれば、一つの変数(許可)にアクセスするシーンを避けられました。そして、案内人が手に許可がないときだけ、トレーを見て、許可があるなら全部自分の手に移動します。集中アクセスするとき、案内人の手には基本許可がなく、処理が終わった人もバトンタッチ優先なので、トレーに許可をなく、1秒ごと動く案内人はトレーをみて(Read)ないなら止まるだけのことになります。

予約サイト用Semaphoreをテストするためのコード

public class ReservationSemaphoreExample {

    private static final Logger logger = LoggerFactory.getLogger(ReservationSemaphore.class);

    static class UserRequest {
        private final long createdAt = System.currentTimeMillis();
        private final int id;

        private final ReservationSemaphore semaphore;
        private final ExecutorService userRequestExecutorService;
        private final CountDownLatch latch;

        UserRequest(int id, ReservationSemaphore semaphore,
                    ExecutorService userRequestExecutorService, CountDownLatch latch) {
            this.id = id;

            this.semaphore = semaphore;
            this.userRequestExecutorService = userRequestExecutorService;
            this.latch = latch;
        }

        void signal() {
            logger.debug("acquired permit, user request {}, delay {}ms", id, System.currentTimeMillis() - createdAt);

            userRequestExecutorService.submit(() -> {
                try {
                    Thread.sleep(new Random().nextInt(100) + 100);
                } catch (InterruptedException ignored) {
                }
                logger.debug("release permit, user request {}", id);
                semaphore.release();
                latch.countDown();
            });
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int requests = 20;
        Random random = new Random();
        CountDownLatch latch = new CountDownLatch(requests);

        ReservationSemaphore semaphore = new ReservationSemaphore(2);
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "semaphore"));
        scheduledExecutorService.scheduleAtFixedRate(semaphore, 0, 500, TimeUnit.MILLISECONDS);

        ScheduledExecutorService userRequestExecutorService1 = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "sender"));
        ExecutorService userRequestExecutorService2 = Executors.newFixedThreadPool(6);
        for (int i = 0; i < requests; i++) {
            final int id = i + 1;
            userRequestExecutorService1.schedule(() -> {
                long startedAt = System.currentTimeMillis();
                semaphore.acquire(new UserRequest(id, semaphore, userRequestExecutorService2, latch));
                logger.info("acquire permit, user request {}, time {}ms", id, System.currentTimeMillis() - startedAt);
            }, random.nextInt(100) + 100, TimeUnit.MILLISECONDS);
        }

        latch.await();

        userRequestExecutorService1.shutdown();
        userRequestExecutorService1.awaitTermination(3, TimeUnit.SECONDS);

        userRequestExecutorService2.shutdown();
        userRequestExecutorService2.awaitTermination(3, TimeUnit.SECONDS);

        scheduledExecutorService.shutdown();
        scheduledExecutorService.awaitTermination(3, TimeUnit.SECONDS);
    }
}

ユーザー20人を想定します。許可数は2です。ユーザーが100~200msの間にランダムで来ます。ユーザーの処理時間は100~200msとします。シュミレーション結果はこちらです。

16:23:24.270 [sender] INFO  - acquire permit, user request 7, time 1ms
16:23:24.273 [sender] INFO  - acquire permit, user request 13, time 0ms
16:23:24.274 [sender] INFO  - acquire permit, user request 3, time 0ms
16:23:24.274 [sender] INFO  - acquire permit, user request 20, time 0ms
16:23:24.276 [sender] INFO  - acquire permit, user request 17, time 0ms
16:23:24.277 [sender] INFO  - acquire permit, user request 5, time 0ms
16:23:24.282 [sender] INFO  - acquire permit, user request 8, time 0ms
16:23:24.284 [sender] INFO  - acquire permit, user request 2, time 0ms
16:23:24.284 [sender] INFO  - acquire permit, user request 6, time 0ms
16:23:24.284 [sender] INFO  - acquire permit, user request 16, time 0ms
16:23:24.289 [sender] INFO  - acquire permit, user request 15, time 0ms
16:23:24.293 [sender] INFO  - acquire permit, user request 14, time 0ms
16:23:24.303 [sender] INFO  - acquire permit, user request 19, time 0ms
16:23:24.320 [sender] INFO  - acquire permit, user request 1, time 0ms
16:23:24.320 [sender] INFO  - acquire permit, user request 4, time 0ms
16:23:24.324 [sender] INFO  - acquire permit, user request 12, time 0ms
16:23:24.341 [sender] INFO  - acquire permit, user request 10, time 0ms
16:23:24.344 [sender] INFO  - acquire permit, user request 9, time 0ms
16:23:24.344 [sender] INFO  - acquire permit, user request 18, time 0ms
16:23:24.351 [sender] INFO  - acquire permit, user request 11, time 0ms
16:23:24.661 [semaphore] DEBUG - acquired permit, user request 7, delay 392ms
16:23:24.662 [semaphore] DEBUG - acquired permit, user request 13, delay 389ms
16:23:24.767 [pool-1-thread-1] DEBUG - release permit, user request 7
16:23:24.767 [pool-1-thread-1] DEBUG - acquired permit, user request 3, delay 493ms
16:23:24.816 [pool-1-thread-2] DEBUG - release permit, user request 13
16:23:24.817 [pool-1-thread-2] DEBUG - acquired permit, user request 20, delay 543ms
16:23:24.930 [pool-1-thread-3] DEBUG - release permit, user request 3
16:23:24.931 [pool-1-thread-3] DEBUG - acquired permit, user request 17, delay 655ms
16:23:24.983 [pool-1-thread-4] DEBUG - release permit, user request 20
16:23:24.983 [pool-1-thread-4] DEBUG - acquired permit, user request 5, delay 706ms
16:23:25.039 [pool-1-thread-5] DEBUG - release permit, user request 17
16:23:25.039 [pool-1-thread-5] DEBUG - acquired permit, user request 8, delay 757ms
16:23:25.109 [pool-1-thread-6] DEBUG - release permit, user request 5
16:23:25.109 [pool-1-thread-6] DEBUG - acquired permit, user request 2, delay 825ms
16:23:25.227 [pool-1-thread-1] DEBUG - release permit, user request 8
16:23:25.227 [pool-1-thread-1] DEBUG - acquired permit, user request 6, delay 943ms
16:23:25.261 [pool-1-thread-6] DEBUG - release permit, user request 2
16:23:25.261 [pool-1-thread-6] DEBUG - acquired permit, user request 16, delay 977ms
16:23:25.369 [pool-1-thread-1] DEBUG - release permit, user request 6
16:23:25.369 [pool-1-thread-1] DEBUG - acquired permit, user request 15, delay 1080ms
16:23:25.416 [pool-1-thread-6] DEBUG - release permit, user request 16
16:23:25.416 [pool-1-thread-6] DEBUG - acquired permit, user request 14, delay 1123ms
16:23:25.519 [pool-1-thread-1] DEBUG - release permit, user request 15
16:23:25.519 [pool-1-thread-1] DEBUG - acquired permit, user request 19, delay 1216ms
16:23:25.607 [pool-1-thread-6] DEBUG - release permit, user request 14
16:23:25.607 [pool-1-thread-6] DEBUG - acquired permit, user request 1, delay 1287ms
16:23:25.678 [pool-1-thread-1] DEBUG - release permit, user request 19
16:23:25.678 [pool-1-thread-1] DEBUG - acquired permit, user request 4, delay 1358ms
16:23:25.774 [pool-1-thread-6] DEBUG - release permit, user request 1
16:23:25.774 [pool-1-thread-6] DEBUG - acquired permit, user request 12, delay 1450ms
16:23:25.879 [pool-1-thread-1] DEBUG - release permit, user request 4
16:23:25.879 [pool-1-thread-1] DEBUG - acquired permit, user request 10, delay 1538ms
16:23:25.908 [pool-1-thread-6] DEBUG - release permit, user request 12
16:23:25.908 [pool-1-thread-6] DEBUG - acquired permit, user request 9, delay 1564ms
16:23:25.990 [pool-1-thread-1] DEBUG - release permit, user request 10
16:23:25.990 [pool-1-thread-1] DEBUG - acquired permit, user request 18, delay 1646ms
16:23:26.067 [pool-1-thread-6] DEBUG - release permit, user request 9
16:23:26.067 [pool-1-thread-6] DEBUG - acquired permit, user request 11, delay 1716ms
16:23:26.174 [pool-1-thread-1] DEBUG - release permit, user request 18
16:23:26.174 [pool-1-thread-6] DEBUG - release permit, user request 11

最初全員列に並んで、瞬時レスポンスをもらえたんです。そして先頭の7番と13番先に案内人から許可をもらいます。あとですべてバトンタッチです。待ち時間一番長かったのは11番1716msです。

続いてガラガラの状況を見てみましょう。集中アクセス対応とはいえ、あまり人がいないときにもうまく動作する必要があります。

15:09:24.161 [sender] INFO  - acquire permit, user request 12, time 1ms
15:09:24.209 [sender] INFO  - acquire permit, user request 16, time 0ms
15:09:24.267 [sender] INFO  - acquire permit, user request 9, time 0ms
15:09:24.556 [semaphore] DEBUG - acquired permit, user request 12, delay 396ms
15:09:24.557 [semaphore] DEBUG - acquired permit, user request 16, delay 348ms
15:09:24.670 [pool-1-thread-2] DEBUG - release permit, user request 16
15:09:24.671 [pool-1-thread-2] DEBUG - acquired permit, user request 9, delay 404ms
15:09:24.756 [pool-1-thread-1] DEBUG - release permit, user request 12
15:09:24.846 [pool-1-thread-3] DEBUG - release permit, user request 9
15:09:25.055 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:25.245 [sender] INFO  - acquire permit, user request 2, time 0ms
15:09:25.520 [sender] INFO  - acquire permit, user request 5, time 0ms
15:09:25.557 [semaphore] DEBUG - acquired permit, user request 2, delay 312ms
15:09:25.558 [semaphore] DEBUG - acquired permit, user request 5, delay 38ms
15:09:25.570 [sender] INFO  - acquire permit, user request 13, time 0ms
15:09:25.682 [pool-1-thread-5] DEBUG - release permit, user request 5
15:09:25.682 [pool-1-thread-4] DEBUG - release permit, user request 2
15:09:25.682 [pool-1-thread-4] DEBUG - acquired permit, user request 13, delay 112ms
15:09:25.796 [pool-1-thread-6] DEBUG - release permit, user request 13
15:09:25.876 [sender] INFO  - acquire permit, user request 7, time 0ms
15:09:25.876 [sender] INFO  - acquire permit, user request 4, time 0ms
15:09:26.056 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:26.057 [semaphore] DEBUG - acquired permit, user request 7, delay 181ms
15:09:26.057 [semaphore] DEBUG - acquired permit, user request 4, delay 181ms
15:09:26.145 [sender] INFO  - acquire permit, user request 8, time 0ms
15:09:26.188 [pool-1-thread-2] DEBUG - release permit, user request 7
15:09:26.188 [pool-1-thread-2] DEBUG - acquired permit, user request 8, delay 43ms
15:09:26.201 [pool-1-thread-1] DEBUG - release permit, user request 4
15:09:26.374 [pool-1-thread-2] DEBUG - release permit, user request 8
15:09:26.495 [sender] INFO  - acquire permit, user request 11, time 0ms
15:09:26.558 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:26.558 [semaphore] DEBUG - acquired permit, user request 11, delay 63ms
15:09:26.682 [pool-1-thread-5] DEBUG - release permit, user request 11
15:09:26.957 [sender] INFO  - acquire permit, user request 14, time 0ms
15:09:27.012 [sender] INFO  - acquire permit, user request 18, time 0ms
15:09:27.057 [semaphore] DEBUG - acquired permit, user request 14, delay 100ms
15:09:27.058 [semaphore] DEBUG - move reserved permits 1 to available permits
15:09:27.058 [semaphore] DEBUG - acquired permit, user request 18, delay 47ms
15:09:27.176 [pool-1-thread-4] DEBUG - release permit, user request 14
15:09:27.196 [pool-1-thread-6] DEBUG - release permit, user request 18
15:09:27.305 [sender] INFO  - acquire permit, user request 3, time 0ms
15:09:27.316 [sender] INFO  - acquire permit, user request 15, time 0ms
15:09:27.558 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:27.558 [semaphore] DEBUG - acquired permit, user request 3, delay 253ms
15:09:27.558 [semaphore] DEBUG - acquired permit, user request 15, delay 242ms
15:09:27.568 [sender] INFO  - acquire permit, user request 6, time 0ms
15:09:27.683 [pool-1-thread-3] DEBUG - release permit, user request 3
15:09:27.683 [pool-1-thread-3] DEBUG - acquired permit, user request 6, delay 115ms
15:09:27.720 [pool-1-thread-1] DEBUG - release permit, user request 15
15:09:27.881 [pool-1-thread-3] DEBUG - release permit, user request 6
15:09:27.895 [sender] INFO  - acquire permit, user request 19, time 0ms
15:09:28.054 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:28.054 [semaphore] DEBUG - acquired permit, user request 19, delay 159ms
15:09:28.219 [pool-1-thread-5] DEBUG - release permit, user request 19
15:09:28.291 [sender] INFO  - acquire permit, user request 1, time 0ms
15:09:28.516 [sender] INFO  - acquire permit, user request 10, time 0ms
15:09:28.539 [sender] INFO  - acquire permit, user request 17, time 0ms
15:09:28.556 [semaphore] DEBUG - acquired permit, user request 1, delay 264ms
15:09:28.556 [semaphore] DEBUG - move reserved permits 1 to available permits
15:09:28.556 [semaphore] DEBUG - acquired permit, user request 10, delay 40ms
15:09:28.678 [pool-1-thread-4] DEBUG - release permit, user request 1
15:09:28.678 [pool-1-thread-4] DEBUG - acquired permit, user request 17, delay 139ms
15:09:28.712 [pool-1-thread-6] DEBUG - release permit, user request 10
15:09:28.840 [pool-1-thread-4] DEBUG - release permit, user request 17
15:09:28.972 [sender] INFO  - acquire permit, user request 20, time 0ms
15:09:29.054 [semaphore] DEBUG - move reserved permits 2 to available permits
15:09:29.054 [semaphore] DEBUG - acquired permit, user request 20, delay 82ms
15:09:29.194 [pool-1-thread-1] DEBUG - release permit, user request 20

ユーザーが来る時間を調整して、結構の頻度で案内人がトレーを見て、処理が終わった人からの許可を自分の手に戻して先頭に渡したことがわかります。

ここまで話したコードはユーザーの処理時間が正常なパターンです。もしものすごく時間かかっているユーザーがいたら、許可を無効化して新しい許可を発行することも難しくありません。時間のためここでコードを書きませんが、改修点は

  1. ユーザーに正常無効など状態をつける(AtomicInteger, 0 = 正常、1 = 終了、2 = 許可無効)
  2. 案内人が1秒ごと動くとき、利用中のユーザーの時間を確認して、時間オーバーしたら許可無効に変えてみて(CAS, compare and swap、0 -> 2)、失敗したらスキップする、成功でしたら、手元にある許可を増やす
  3. 処理し終わったユーザーは自分の状態を終了にしてみて、失敗したら何もせず終わる

最後に、私が書いていたコードは実際のチケット予約サイトに使ったことがないので、ユーザーへの通知などいろいろ調整が必要です。集中アクセスの特徴を利用して作ってみましたが、Webサイトがメインの人はRedisなどを利用するかもしれません。個人的にはキューが入ると取り出すスレッドが別々なので、シングルスレッドRedisに入れてもとくに大きな影響がないと思います。ただ許可もRedisに入れるのは難しいかもしれません。案内人や処理が終わった人は同時に行動できるので、別途持たせたほうがいいです。そして自分のコードは意識てしロックなしで書いていたのですが、すべてRedisにいれると大きなロックをかけたようなことになってしまいます。

いかがでしょうか。チケット予約サイトの仕組みについてすこしわかってましたでしょうか。Webサイトよりもうちょっと並列処理に近い問題ですが、一般的な使い方Semaphoreと違っても、そのアイディアを受継いて改善して、問題の特徴を利用して新しい解決方法を作れるのは面白いと思います。この記事がすこし参考になれば幸いです。

仲間を募集中!

ユニファでは、仲間を募集しています。

ご興味をお持ちの方はぜひ一度、お話を聞きにきてください。

unifa-e.com