分散ロック-redLock And Redisson

31374 ワード

文書ディレクトリ
  • CAP
  • redis分散ロックの問題
  • RedLock
  • RedLockって何ですか?
  • RedLockアルゴリズム
  • 失敗時再試行
  • リリースロック
  • パフォーマンス、クラッシュリカバリ、およびredis同期
  • redlockに対する紛争
  • Redisson
  • 使用例
  • ソースコード
  • CAP
    RedLockを引き出す前に,分布系におけるCAP理論を紹介する.
    C(Consistency):コンシステンシは、同じ時点ですべてのノードのデータが完全に一致します.
    A(Availability):可用性は、通常の時間内にリクエストに応答できるはずです.
    P(Partition-tolerance):パーティションの許容性、分散環境では、複数のノードからなるネットワークが相互に接続されているべきであり、ネットワーク障害などの原因でネットワークパーティションが発生した場合、外部にサービスを提供することが要求される.
    CAP理論は,どの分散系も3つのうち2つしか満たされず,すべてが満たされることは不可能であることを示した.
    分布式システムを参照すること
    redis分散ロックの問題点
    ネット上ではredisを用いて分散ロックを実現するには多くの案があり,比較的完備した案はsetNx+luaで実現すべきである.以下のように簡単に実現されます.
    - java  -  ,   setnx lock_key_name unique_value
    set lock_key_name unique_value NX PX 5000;
    - lua  -  ,     
    if redis.call("get", KEYS[1] == ARGV[1]) then
    	return redis.call("del", KEYS[1])
    else
        return 0
    end
    

    注意:
  • valueは一意性を必要とし、タイムスタンプ、uuidまたは自己増加idを用いて実現することができる.
  • クライアントは、ロックを解除する際に、ローカルメモリのvalueとredisのvalueが一致しているかどうかを比較し、誤ロックを防止する必要があります.(case:clientAはロックロックロック1を取得し、clientAが実行する時間が比較的長いため、key=lock 1が期限切れになり、redisインスタンスはこのkeyを削除する.clientBは同じロックロックロックlock 1を取得し、clientBはロックを占有して業務を実行している.この時、clientA業務はすでに実行済みで、ロックを解放する準備ができている.valueを比較する論理がなければ、clientAはclientBが持っているロックを解放する.これは明らかにだめです.value値が異なるため、clientAがロックを解除するときは自分が追加したロックだけを解放し、他のクライアントが追加したロックを誤って解放しません)
  • 分散システムでは、単一の障害を回避し、信頼性を高めるためにredisはプライマリ・スレーブ・アーキテクチャを採用し、プライマリ・ノードが停止すると、セカンダリ・ノードがプライマリとしてサービスを継続します.このシナリオは、ほとんどのビジネスシーンを満たすことができますが、取引などの一貫性が要求されるシナリオには、次の理由で脆弱性があります.
    redisプライマリスレーブアーキテクチャは非同期レプリケーションを採用しており、masterノードがロックを取得したが、ロックがslaveノードに同期されていない場合、masterノードが停止し、フェイルオーバが発生し、slaveノードがmasterノードとして選択され、ロックが失われた.これにより、他のスレッドがロックを取得でき、明らかに問題があります.
    従って、上述したredisベースの分散ロックは、APのみを満たし、Cを満たしていない.
    RedLock
    上述したredis分散ロックの一貫性の問題のために,redis著者らは,redisに基づいて実現されるより高度な分散ロックであるRedLockを提案した.原文はDistributed locks with Redisを参照可能
    RedLockって何?
    RedLockはredisに基づいて実装された分散ロックであり、以下の特性を保証することができる.
  • 反発性:いつでも、ロックを保持できるクライアントは1つしかありません.
  • デッドロックを避ける:クライアントがロックを手に入れた後、ネットワークパーティションやクライアントのダウンタイムが発生しても、デッドロックは発生しません.(keyを利用した生存時間)
  • フォールトトレランス:多くのノードのredisインスタンスが正常に動作している限り、外部にサービスを提供し、ロックを追加または解放することができる.

  • redLockではなく反発性を満たすことができないのは,上述した理由である.
    RedLockアルゴリズム
    N個のredisのmasterノードがあると仮定し、これらのノードは互いに独立している(主従や他の協調を必要としないシステム).Nは奇数~
    クライアントは、ロックを取得する際に、次の操作を行う必要があります.
  • 現在のタイムスタンプを取得し、微妙に単一です.
  • 同じlockNameとlockValueを使用して、N個のノードからロックを取得しようとします.(ロックを取得する場合、ロックの解放時間よりもロックの解放時間を待つ必要がある.例えば、ロックのlease_timeが10 sである場合、wait_timeは5~50ミリ秒であるべきである.redisインスタンスが停止したため、クライアントはfast_failを満たすためにより長い時間待つ必要があることを避ける.redisインスタンスが使用できない場合、次のredisインスタンスから取得を継続する必要があるロック)
  • N個のノードからのロック取得が終了すると、クライアントが多数のノード(N/2+1)からロックを正常に取得することができ、ロック取得時間が失効時間よりも小さい場合、クライアントがロックを正常に取得したと考えられる.(ロック取得時刻=現在のタイムスタンプ-ステップ1のタイムスタンプ)
  • クライアントがロックを正常に取得した後、ロックの実際の有効時間=ロックの有効時間を設定する-ロックを取得する時間.
  • クライアントがロックを取得できなかった場合、Nノードのredisインスタンスは、ロックが正常にロックされなかった場合でもロックを解放します.

  • どうしてNが奇数を推薦するのですか?原因1:最大フォールトトレランスの場合、サービス資源の占有が最も少ないという原則に基づいて、2 N+1と2 N+2のフォールトトレランス能力は同じであるため、2 N+1を採用する.例えば,5台のサーバで2台のダウンタイムが許可され,フォールトトレランスは2,6台のサーバでも2台のダウンタイムしか許可されず,フォールトトレランスも2であり,ノードの半数以上の生存が要求されるためOKである.
    理由2:6つのredisノードがあり、client 1とclient 2が同時にredisインスタンスに同じロックリソースを取得していると仮定すると、client 1が3つのロックを取得し、client 2が3つのロックを取得し、いずれも半数を超えていないため、client 1とclient 2がロックを取得するのに失敗し、奇数ノードにはこの問題は存在しない.
    失敗時に再試行
    クライアントがロックを取得できない場合は、ランダムに遅延して再試行する必要があります.複数のクライアントが同じ時間に同じリソースのロックを奪うことを防止する(脳裂を招き、最終的にはロックを取得できない).クライアントが半数を超えるノードのロックを取得するのにかかる時間が短いほど、脳裂の確率は低くなる.したがって、理想的には、クライアントは同時に(同時)すべてのredisにsetコマンドを発行することが望ましい.
    クライアントが多数のノードからロックを取得できない場合は、正常に取得したロックをできるだけ早く解放する必要があります.これにより、他のクライアントはロックが期限切れになってから取得する必要がありません.(ネットワークパーティションが存在し、クライアントがredisと通信できない場合は、ロックが期限切れになった後に自動的に解放されるのを待つしかありません)
    なぜ脳裂が起こったのか分からない???
    リリースロック
    すべてのredisインスタンスにロック解除コマンドを送信すれば、redisインスタンスが正常にロックされているかどうかを気にする必要はありません.
    redissonはロックをかける時、key=lockName、value=uuid+threadID、set構造を採用して保存して、そしてロックをかける回数(再入可能を支持する)を含んでいる;ロックを解く時hexistsを通じてkeyとvalueが存在するかどうかを判断して、存在するならロックを解く;ここで誤ロックは現れない
    パフォーマンス、クラッシュ・リカバリ、redis同期
    分散ロックのパフォーマンスを向上させるにはどうすればいいですか?パフォーマンス指標としてacquire/release操作を毎分何回実行するかを示し、一方でredisインスタンスを増加させることで応答遅延を低減できる一方で、非ブロックモデルを使用して、すべてのコマンドを一度に送信し、その後、応答結果を非同期で読み出す.ここでは、クライアントとredis間のRTT差が少ないと仮定する.
    redisがバックアップを使用しない場合、redisが再起動するとロックが失われ、複数のクライアントがロックを取得できます.AOF持続化によりこの問題を緩和できる.redis keyの期限切れはunixタイムスタンプであり、redisが再起動しても時間は前進する.でも、停電だったら?redisが起動すると、このkeyが失われる可能性があります(ディスクに書き込まれているか、まだ書き込まれていないときに電源が切れています.fsyncの構成に依存します).fsync=alwaysを使用すると、パフォーマンスに大きな影響を与えます.この問題をどのように解決すればいいですか?redisノードを再起動した後、1つのTTL期間でクライアントに使用できなくなります.
    redlockに対する論争
    その後、この部分の内容が更新されます.[参照リンク](http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
    Redisson
    redissonはredisに基づいて実現されたオープンソースソリューションであり、一連の分散java共通オブジェクトを提供するだけでなく、多くの分散サービスを提供し、ユーザーのredisへの関心の分離を促進し、ビジネスロジックの処理にもっと関心を持つことを目的としている.
    redissonもredlockに対して一連の実現を行い、詳細は以下の通りである.
    使用例
    public static void main() {
    
            Config config1 = new Config();
            config1.useSingleServer().setAddress("redis://xxxx1:xxx1")
                    .setPassword("xxxx1")
                    .setDatabase(0);
            RedissonClient redissonClient1 = Redisson.create(config1);
            Config config2 = new Config();
            config2.useSingleServer()
                    .setAddress("redis://xxxx2:xxx2")
                    .setPassword("xxxx2")
                    .setDatabase(0);
    
            RedissonClient redissonClient2 = Redisson.create(config2);
    
            Config config3 = new Config();
            config3.useSingleServer().
                    setAddress("redis://xxxx3:xxx3")
                    .setPassword("xxxx3")
                    .setDatabase(0);
    
            RedissonClient redissonClient3 = Redisson.create(config3);
    
            String lockName = "redlock-test";
            RLock lock1 = redissonClient1.getLock(lockName);
            RLock lock2 = redissonClient2.getLock(lockName);
            RLock lock3 = redissonClient3.getLock(lockName);
    
            RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
            boolean isLock;
            try {
                isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);
                System.out.println("isLock = " + isLock);
                if (isLock) {
                    // lock success, do something;
                    Thread.sleep(30000);
                }
            } catch (Exception e) {
    
            } finally {
                //     ,       
                redLock.unlock();
                System.out.println("unlock success");
            }
        }
    

    ソースコード
    tryLock():redissonのredlockの実装方式は基本的に上記の説明と類似しており、redissonがロックの取得に成功した後、keyの失効時間を再する点で異なる.
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
            long newLeaseTime = -1;
            if (leaseTime != -1) {
                newLeaseTime = unit.toMillis(waitTime)*2;
            }
            
            long time = System.currentTimeMillis();
            long remainTime = -1;
            if (waitTime != -1) {
                remainTime = unit.toMillis(waitTime);
            }
            long lockWaitTime = calcLockWaitTime(remainTime);
            
            int failedLocksLimit = failedLocksLimit();
            List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
            for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
                RLock lock = iterator.next();
                boolean lockAcquired;
                try {
                    if (waitTime == -1 && leaseTime == -1) {
                        lockAcquired = lock.tryLock();
                    } else {
                        long awaitTime = Math.min(lockWaitTime, remainTime);
                        lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                    }
                } catch (RedisResponseTimeoutException e) {
                    unlockInner(Arrays.asList(lock));
                    lockAcquired = false;
                } catch (Exception e) {
                    lockAcquired = false;
                }
                
                if (lockAcquired) {
                    acquiredLocks.add(lock);
                } else {
                    if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                        break;
                    }
    
                    if (failedLocksLimit == 0) {
                        unlockInner(acquiredLocks);
                        if (waitTime == -1 && leaseTime == -1) {
                            return false;
                        }
                        failedLocksLimit = failedLocksLimit();
                        acquiredLocks.clear();
                        // reset iterator
                        while (iterator.hasPrevious()) {
                            iterator.previous();
                        }
                    } else {
                        failedLocksLimit--;
                    }
                }
                
                if (remainTime != -1) {
                    remainTime -= (System.currentTimeMillis() - time);
                    time = System.currentTimeMillis();
                    if (remainTime <= 0) {
                        unlockInner(acquiredLocks);
                        return false;
                    }
                }
            }
    
            if (leaseTime != -1) {
                List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
                for (RLock rLock : acquiredLocks) {
                    RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                    futures.add(future);
                }
                
                for (RFuture<Boolean> rFuture : futures) {
                    rFuture.syncUninterruptibly();
                }
            }
            
            return true;
        }