1. ホーム
  2. データベース
  3. レディス

redis分散ロックについて解説(redis分散ロックの最適化処理とRedissonの利用について)

2022-01-20 19:42:27

1.レディスの実際

データのキャッシュに使えるだけでなく、分散アプリケーションの開発では分散ロックとして使われることが多いのですが、なぜ分散ロックを使うのでしょうか?

分散開発では、EC在庫の更新機能を利用して、実際のアプリケーションでは同じ機能を持つコンシューマが複数存在することを説明します。複数のコンシューマが同時にデータを消費しようとする場合、ビジネスロジックの処理ロジックがredisにある商品の在庫を問い合わせるもので、最初に来たコンシューマが在庫を取得し、まだ在庫削減処理を行っていない場合、比較的遅い 最初に来たコンシューマが商品の在庫を取得すると、データに誤りが生じ、さらにデータが消費されることになるのである。

例えば、消費者Aと消費者BがそれぞれプロデューサーC1とプロデューサーC2からデータを消費しに行き、プロデューサーが同じredisデータベースを使っている場合、プロデューサーC1が消費者Aからメッセージを受け取ると、まず在庫の問い合わせを行い、いざ在庫削減を行う際に、プロデューサーC2が消費者Bからメッセージを受け取ったため、こちらも在庫問い合わせに行き、プロデューサーC1が行っていないので これによりプロデューサーC2にはプロデューサーC1から更新したデータではなくダーティデータが入り、ビジネスエラーを引き起こすのだそうです。

アプリケーションが分散していない場合、synchronizedを使用してインベントリ更新の問題が発生するのを防ぐことができますが、synchronizedはJVMレベルのみに基づいているので、異なるJVMにいる場合、そのような機能を実装することはできません。

   @GetMapping("getInt0")
    public String test() {
        synchronized (this) {
            //Get the current number of products
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //then the product is checked out, i.e., minus 1
            /*
             * a business logic
             *
             */
            if (productNum > 0) {
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                int productNumNow = productNum - 1;
            } else {
                return "product=0";
            }
            int productNumNow = productNum - 1;
            return "success=" + productNumNow;
        }
    }



2. redisの機能を利用した分散ロックの実装方法

2.1 Redis の分散ロックの考え方

redisに詳しい人なら、redisのsetnxコマンドはset関数に似ていると思うのですが、setnxコマンドはデータを格納する前に同じキーがすでにredisに存在するかどうかを調べて、存在すれば偽、存在しなければ真を返すので、このコマンドの機能を使って分散ロックを設計することができるのです。

2.1.1 デザインのアイデア

  • インターフェイスの同じ関数の要求では、使用redis setnxコマンドは、この時点ではロックを取得するに等しいこのインターフェイスには、他の呼び出しがないことを示す、真として返された場合、その後、次のビジネスロジックを実行するために続けることができます。ビジネスロジックが終了すると、データが返される前にキーが削除され、他の要求がロックを取得できるようになります。
  • setnxコマンドを使用すると、falseが返されます。これは、他のコンシューマーがこの時点でインターフェースを呼び出していることを意味するので、分散ロックを取得する前に、他のコンシューマーの消費が正常に終了するのを待つ必要があります。

2.1.2 上記の設計思想に基づくコードの実装

コード・スニペット [1

  @GetMapping("getInt1")
    public String fubushisuo(){
        //setIfAbsent command functions the same as setNx in redis command, if the same key already exists in redis, then return false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
        boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        // If the lockkey is successfully set, it means that the distributed lock is currently acquired
        if (!opsForSet){
            return "false";
        }
        //Get the current number of products
        int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
        //then the product is checked out, i.e., minus 1
        /*
        * a business logic
        *
        */
        if (productNum>0){
            stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
            int productNumNow = productNum - 1;
        }else {
            return "product=0";
        }
        //Then do the lock release
        stringRedisTemplate.delete(lockkey);
        int productNumNow = productNum-1;
        return "success="+productNumNow;
    }




2.1.2.1 反射型コードスニペット [1

デッドロックが発生するような使い方をした場合。
デッドロックは、以下の場合に発生します。
(1) ビジネスロジックにエラーがあり、delete() オペレーションが実行できず、他のリクエストが分散ロックを取得できない場合、ビジネスロックキーがリードに存在し続け、setnxオペレーションが常に失敗し、分散ロックを取得できない。
(2) 解決策:ビジネスコードにtry...catchオペレーションを使用し、エラーが発生した場合はfinallyでキーを削除する。

最適化コード [2

     @GetMapping("getInt2")
    public String fubushisuo2(){
        //setIfAbsent command functions the same as setNx in redis command, if the same key already exists in redis, then return false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
        boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        int productNumNow = 0;
        // If the lockkey is successfully set, it means that the distributed lock is currently acquired
        if (!opsForSet){
            return "false";
        }
        try {
            //Get the current number of products
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //then the product is checked out, i.e., minus 1
            /*
             * b business logic
             */
            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
                //then do the lock release
            stringRedisTemplate.delete(lockkey);
        }

        return "success="+productNumNow;
    }


2.1.2.2 Reflection コード [2

問題の発生
このシナリオも、現時点でメソッドを実行しているサーバーが複数ある場合に発生する状況であれば
メソッドの1つが分散ロックを取得し、次のビジネスコードを実行中にそのサーバが突然ダウンし、他のサーバが分散ロックの取得に失敗する。

解決策:有効期限を追加するが、その後サービスが停止し、設定時間後にredisがキーを削除することで、残りのサーバーが正常にロックできるようにします。

最適化コード[3]

 @GetMapping("getInt3")
    public String fubushisuo3(){
        //setIfAbsent command functions the same as setNx in redis command, if the same key already exists in redis, then return false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
       //[01] boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        // Set the expiration time to 10 seconds, but if you use this command, there is no atomicity and it may go down before executing expire instead of setting the expiration time.
       //[02] stringRedisTemplate.expire(lockkey, Duration.ofSeconds(10));
        //use setIfAbsent(lockkey,lockvalue,10,TimeUnit.SECONDS); code instead of [01],[02] lines above
        Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
        int productNumNow = 0;
        // If the lockkey is successfully set, it means that the distributed lock is currently acquired
        if (!opsForSet){
            return "false";
        }
        try {
            //Get the current number of products
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //then the product is checked out, i.e., minus 1
            /*
             * c business logic
             */
            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
        
        //then do the lock release
            stringRedisTemplate.delete(lockkey);
        }
        
        return "success="+productNumNow;
    }



2.1.2.3 最適化されたコードの振り返り [3

問題の発生
cのビジネスロジックが設定時間を超えて持続し、redisのロックキーが失効してしまった場合
そして、他のユーザーがロックを取得するために、この時点でメソッドにアクセスし、この時点で、以前のCビジネスロジックも実行を終了しますが、彼は削除を実行します、lcokkeyが削除されました。この結果、分散ロックエラーになります。
例 例:12:01の瞬間。 55、getInt3メソッドを実行するAがあり、正常にロックを取得しますが、Aは、実行の10秒後にビジネスロジックを終了することはできません、redisでロックを失効させる、11秒でBがあるgetint3メソッドを実行するために、キーはAによって削除され、正常にredisロックを取得できるようになる原因となる。とBはロックを取得した後、Aは実行が完了したため、その後、IDのキーを削除するには、しかし、我々は、ロックを削除するとBはロックに追加され、Aのロックは、有効期限のため、唯一のredis自体によって削除されるので、これはCにつながる場合は、この時点でredisも分散ロックを取得することができるとき

解決策 UUIDを使って乱数を生成し、redisでキーを削除したいときに以前設定したUUIDかどうかを判断する

コードの最適化 [4

  @GetMapping("getInt4")
    public String fubushisuo4(){
        //setIfAbsent command functions the same as setNx in redis command, if the same key already exists in redis, then return false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        //get UUID
        String lockvalue = UUID.randomUUID().toString();
        Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
        int productNumNow = 0;
        // If the lockkey is successfully set, it means that the distributed lock is currently acquired
        if (!opsForSet){
            return "false";
        }
        try {
            //Get the current number of products
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //then the product is checked out, i.e., minus 1
            /*
             * c business logic
             */

            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
        
        // Do the lock release
            if (lockvalue==stringRedisTemplate.opsForValue().get(lockkey)){
                stringRedisTemplate.delete(lockkey);
            }
        }
        return "success="+productNumNow;
    }


2.1.2.4 最適化されたコードの振り返り [4

問題の発生
この時点でこの方法は完璧であり、並行処理が超高速でない場合には使用可能であるが、業務が終了する前に鍵が失効してしまうことを防ぐために、業務実行時間に合わせて鍵の有効期限を設定する必要がある。

解決策 redis用の分散ロックフレームワークはたくさんあり、その中でもredissonはよく使われている

2.2 redissonによる分散ロックの実装

まず、redissonのmaven依存関係を追加します。

        <dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.11.1</version>
		</dependency>


redissonのビーン構成

@Configuration
public class RedissonConfigure {
    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://27.196.106.42:6380").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}



分散ロックコードを次のように実装します。

 @GetMapping("getInt5")
    public String fubushisuo5(){
        //setIfAbsent command functions the same as setNx in redis command, if the same key already exists in redis, then return false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        //get UUID
        RLock lock = redisson.getLock(lockkey);
        lock.lock();
        int productNumNow = 0;
        //If the lockkey is successfully set, it means that the distributed lock is currently acquired

        try {
            //get the current number of products
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //then the product is checked out, i.e., minus 1
            /*
             * c business logic
             */
            if (productNum>0){
            stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
            productNumNow = productNum-1;
            }else {
                return "product=0";
            }
        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
           lock.unlock();
            }

        //then do the lock release
        return "success="+productNumNow;
    }


表面から見てわかるように、redissonはいくつかの簡単なコマンドで分散ロックを実装することができます。
redissonは以下のような原理であれば、分散ロックを実装しています。
redissonは、Luaスクリプト言語を使用して、コマンドの両方をアトミックにするために、redissonは、ロックを取得し、有効期限にキー30秒を設定します押されて、同時にredissonは、現在の要求スレッド番号を記録し、定期的にスレッドの状態を確認するには、まだ実行状態では、ほぼ有効期限にキー場合は、 redissonキー、通常10秒で有効期限の時間を変更することになります。これは、最適化コードの断片を補う、動的にキーの有効期限を設定することが可能になります[4]。

redis分散ロックに関する記事はこちら(redis分散ロックの処理の最適化とRedissonの利用)が全てです。redis分散ロックに関する詳しい情報は、Scripting Houseの過去記事を検索するか、以下の関連記事を引き続きご覧ください。