Redis+Luaによる分散ロック(SpringBoot版)


せっけい構想
分散ロックを実現する以上、複数の接続が1つのリソースを集中的に要求する排他性を保証しなければならないが、redisの単一スレッド特性はこのニーズをよく満たしている.redisが提供するset方法はこの需要を満たす鍵であり、下図はredis分布式ロックを実現する簡単な流れであり、まず初歩的な材料と.
シーン解析setコマンドの使用方法を次に示します.
SET key value [EX seconds] [PX milliseconds] [NX|XX]

オプションのパラメータ:
  • EX second:キーの有効期限をsecond秒に設定します.SET key value EX second効果はSETEX key second valueと同等です.
  • PX millisecond:設定キーの有効期限はmillisecondミリ秒です.SET key value PX millisecond効果はPSETEX key millisecond valueと同等です.
  • NX:キーが存在しない場合のみ、キーの設定操作を行います.SET key value NX効果はSETNX key valueに等しい.
  • XX:キーが既に存在する場合のみ、キーを設定します.

  • 簡単な実現のように見えますが、実際には隠れた穴がたくさんあります.以下、いくつかのケースを分析します.
    Case1 :
    redis命令に詳しい学生は気づくかもしれませんsetnxこの命令は、その後、協力します.expire命令は同じ効果を実現できますか?
    SETEX user_id 10086
    //        ,  user_id   
    expire user_id

    注意、これは2つのコマンドであり、これは非原子的な操作を意味し、第1のコマンドSETEX user_id 10086を実行し、このキーに失効時間を設定する準備をしている間にサービスが突然停止すると、このキーは永遠に存在し、他のリクエストは永遠にこのロックを取得できません.SETで原子性を保証する必要がありますSET user_id 10086 EX 30 NX、すなわちkeyをuser_に設定するid,valueは10086であり、失効時間は30 sに設定され、keyが存在する場合は変更を放棄する.
    Case2 :
    私たちがkey値を設定するときは、注文ID、在庫IDなど、できるだけ彼の一意性を保証します.によってSET命令の戻り結果は、他の要求が強占されているかどうかを判断し、value値の設定があってもなくてもよいようですが、事実は本当ですか?
    疑似コード:
    SET user_id 10086 EX 30 NX
    //      
    
    //      
    DEL user_id

    フローチャート分析:
    上記の図では、リクエストされたプロセスを以下のステップに分けます.
  • の3つの要求A,B,Cは同時にロックを競合し、要求Aによって先に取得され、他の要求は絶えずロック
  • を取得しようとするしかない.
  • 要求Aは、業務の複雑な処理時間がタイムアウトしたため、要求Bはロック
  • を取得することができる.
  • 要求Aはついに自分の業務を完成し、この時DEL user_idを実行したが、彼自身のロックはすでに失効し、削除したのは要求Bロックである.要求Bの業務はこの時点で処理されていないので、ここで問題が発生しました.

  • 改善:他人のロックを誤って削除することを避けるために、ロックを削除するときにこのロックが自分のものかどうかを判断する必要があります.このとき私たちが設定したvalueが有効になり、valueによってこのロックが自分のものかどうかを判断することができます.このvalue値の設定は比較的勝手で、区別さえできればいいです.
    疑似コード:
    SET user_id 10086 EX 30 NX
    //      
    
    //      
    if( (GET user_id) == "XXX" ){
      DEL user_id
    }

    Case3 :
    よし、やっとこの一連の穴を解決した.もうすぐ完成すると思っていた.自分が改良したコードを得意げに吟味していると、どこか変な感じがして、このGETの取値判断やDEL削除は原子操作ではないことに気づいた.では、上の分析に続いて、どんな問題が発生しますか?
    if( (GET user_id) == "XXX" ){ //       ,           。  ,       。
      DEL user_id
    }

    プログラムがこのロックの値を判断して、このロックが自分で追加されたことを発見したら、DELを準備する.このとき、ロックは適切に失効し、別のリクエストはkey値user_を適切に取得します.idのロック.この时、プログラムはDEL user_idを実行して、他の人のロックを削除して、気まずいです!したがって、このコードは完璧ではありません.クエリーと削除の原子的な操作を保証するためにluaスクリプトサポートを導入する必要があります.
    改善:
    String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

    補足
    Case 2に戻ると,要求Aにおける業務処理時間がロックの失効時間を超えた.このような問題に対して、多くのネット上の大物が与えた答えは、keyを監視するためのガードスレッドの失効時間を作り、失効寸前に継続することです.
    実はredisは分布式ロックに対してRedissonはより良い実現方式を持っている.
    コード(後でアップロード....)