Java で独自の Redis 分散ロックをゼロから実装する

Java で独自の Redis 分散ロックをゼロから実装する

[[347022]]

Redis 分散ロック

分散ロックはなぜ必要なのでしょうか?

JDK はロックする方法を提供します:

(1)同期キーワード

(2)揮発性+CASによる楽観的ロックの実装

(3)リードライトロック

(4) ReenTrantLock 再入可能ロック

待ってください、これらのロックは非常に便利であり、マルチスレッドの状況でスレッドの安全性を保証します。

しかし、分散システムでは、上記のロックは役に立ちません。

分散システムにおける同時実行の問題を解決したい場合は、分散ロックの概念を導入する必要があります。

Javaコードの実装

モチベーション

まず第一に、ロック実装の原則の実現です。理論は実践を導き、実践は理論を向上させます。

夕方にはRedis分散ロックに関する記事がたくさんありますが、その質はさまざまです。

ミドルウェア チームが Redis 分散ロック ツールを提供していない場合があり、提供されている場合でも頻繁にメンテナンスされない場合があります。自分で実装して原理を理解し、変更しやすくする方がよいでしょう。

インターフェース定義

JDK での再利用を容易にするために、インターフェースは JDK の Lock インターフェースから継承します。

  1. パッケージcom.github.houbb.lock.api.core;
  2.  
  3. java.util.concurrent.TimeUnitをインポートします
  4. java.util.concurrent.locks.Lockをインポートします
  5.  
  6. /**
  7. * ロックの定義
  8. * @author binbin.hou
  9. * @0.0.1 以降
  10. */  
  11. 公共 インターフェースILockはLockを拡張します{
  12.  
  13. /**
  14. * ロックを試みる
  15. * @param 時間 時間
  16. * @paramの単位は
  17. * @param キー キー
  18. * @return 戻り
  19. * @throws InterruptedException例外
  20. * @0.0.1 以降
  21. */  
  22. boolean tryLock( long時間, TimeUnit 単位,
  23. 文字列キー) はInterruptedExceptionをスローします
  24.  
  25. /**
  26. * ロックを試みる
  27. * @param キー キー
  28. * @return 戻り
  29. * @0.0.1 以降
  30. */  
  31. ブール型tryLock(文字列キー);
  32.  
  33. /**
  34. * ロック解除
  35. * @param キー キー
  36. * @0.0.1 以降
  37. */  
  38. void unlock(文字列キー);
  39.  
  40. }

メソッド シンプルさを保つために、最初のバージョンではよく使用される 3 つのコア メソッドのみを追加しました。

後でさらに追加することもできます。

抽象実装

後でさらに実装を追加しやすくするために、共通の抽象親クラスが最初にここで実装されます。

  1. パッケージcom.github.houbb.lock.redis.core;
  2.  
  3. com.github.houbb.lock.api.core.ILockをインポートします
  4. com.github.houbb.lock.redis.constant.LockRedisConstをインポートします
  5. com.github.houbb.wait.api.IWaitをインポートします
  6.  
  7. java.util.concurrent.TimeUnitをインポートします
  8. java.util.concurrent.locks.Conditionをインポートします
  9.  
  10. /**
  11. * 抽象実装
  12. * @author binbin.hou
  13. * @0.0.1 以降
  14. */  
  15. 公共 抽象的な クラスAbstractLockRedisはILockを実装します{
  16.  
  17. /**
  18. * ロック待機
  19. * @0.0.1 以降
  20. */  
  21. プライベート 最終的なIWait待機;
  22.  
  23. 保護されたAbstractLockRedis(IWait wait) {
  24. this .wait = wait;
  25. }
  26.  
  27. @オーバーライド 
  28. 公共  voidロック() {
  29. 投げる 新しいUnsupportedOperationException();
  30. }
  31.  
  32. @オーバーライド 
  33. 公共  void lockInterruptibly()InterruptedExceptionをスローします{
  34. 投げる 新しいUnsupportedOperationException();
  35. }
  36.  
  37. @オーバーライド 
  38. 公共 ブール型tryLock() {
  39. tryLock(LockRedisConst.DEFAULT_KEY)を返します
  40. }
  41.  
  42. @オーバーライド 
  43. 公共  voidロック解除() {
  44. LockRedisConst.DEFAULT_KEY のロックを解除します。
  45. }
  46.  
  47. @オーバーライド 
  48. 公共  boolean tryLock( long time, TimeUnit unit, String key)InterruptedExceptionをスローします{
  49. 長いstartTimeMills = System.currentTimeMillis();
  50.  
  51. // 一度取得して直接成功します 
  52. ブール結果 = this .tryLock(key);
  53. if (結果) {
  54. 戻る 真実;
  55. }
  56.  
  57. // 時間判断 
  58. 時間 <= 0場合
  59. 戻る 間違い;
  60. }
  61. 長い期間Mills = unit.toMillis(時間);
  62. 長いendMills = startTimeMills +durationMills;
  63.  
  64. // ループ待機 
  65. (System.currentTimeMillis() < endMills)の間{
  66. 結果 = tryLock(キー);
  67. if (結果) {
  68. 戻る 真実;
  69. }
  70.  
  71. // 10ms待つ 
  72. 待機します。待機します(TimeUnit.MILLISECONDS、 10 );
  73. }
  74. 戻る 間違い;
  75. }
  76.  
  77. @オーバーライド 
  78. 公共 同期した  boolean tryLock( long time, TimeUnit unit)InterruptedExceptionをスローします{
  79. tryLock(time, unit, LockRedisConst.DEFAULT_KEY) を返します
  80. }
  81.  
  82. @オーバーライド 
  83. public条件 newCondition() {
  84. 投げる 新しいUnsupportedOperationException();
  85. }
  86.  
  87. }

実際のコアは、public boolean tryLock(long time, TimeUnit unit, String key) throws InterruptedException メソッドです。

このメソッドは、ロックを取得するために this.tryLock(key) を呼び出します。成功した場合は直接戻ります。失敗した場合はループで待機します。

ここでタイムアウトを設定します。タイムアウトした場合は、直接 true が返されます。

Redis ロックの実装

私たちが実装した Redis 分散ロックは、上記の抽象クラスから継承されます。

  1. パッケージcom.github.houbb.lock.redis.core;
  2.  
  3. com.github.houbb.heaven.util.lang.StringUtilをインポートします
  4. com.github.houbb.id.api.Idをインポートします
  5. com.github.houbb.id.core.util.IdThreadLocalHelperをインポートします
  6. com.github.houbb.lock.redis.constant.LockRedisConstをインポートします
  7. com.github.houbb.lock.redis.exception.LockRedisExceptionをインポートします
  8. com.github.houbb.lock.redis.support.operator.IOperatorをインポートします
  9. com.github.houbb.wait.api.IWaitをインポートします
  10.  
  11. /**
  12. * これはRedis実装に基づいています
  13. *
  14. ※実際にはzk/databaseなどをベースに実装することも可能です。
  15. *
  16. * @author binbin.hou
  17. * @0.0.1 以降
  18. */  
  19. 公共  LockRedisクラスはAbstractLockRedisを拡張します
  20.  
  21. /**
  22. * Redis操作の実装
  23. * @0.0.1 以降
  24. */  
  25. プライベート 最終的なIOperator redisOperator;
  26.  
  27. /**
  28. * 主キー識別子
  29. * @0.0.1 以降
  30. */  
  31. プライベート 最終ID ID;
  32.  
  33. パブリックLockRedis(IWait wait、IOperator redisOperator、Id id) {
  34. スーパー(待つ)
  35. これは.redisOperator = redisOperator です。
  36. この.id = id;
  37. }
  38.  
  39. @オーバーライド 
  40. 公共 ブール型tryLock(文字列キー) {
  41. 最終的な文字列 requestId = id.id();
  42. IdThreadLocalHelper.put(リクエストId);
  43.  
  44. redisOperator.lock(キー、requestId、LockRedisConst.DEFAULT_EXPIRE_MILLS)を返します
  45. }
  46.  
  47. @オーバーライド 
  48. 公共  void unlock(文字列キー) {
  49. 最終的な文字列 requestId = IdThreadLocalHelper.get();
  50. StringUtil.isEmpty(リクエストID)の場合{
  51. 文字列 threadName = Thread.currentThread().getName();
  52. 投げる 新しいLockRedisException( "スレッド " + threadName + " に requestId が含まれていません" );
  53. }
  54.  
  55. ブール値のロック解除 = redisOperator.unlock(キー、リクエスト ID);
  56. ロック解除の場合
  57. 投げる  new LockRedisException( "キー " + キー + " のロック解除結果が失敗しました!" );
  58. }
  59. }
  60. }

これが redis lock のコア実装です。理解できない場合は、原則を確認することをお勧めします。

Redis 分散ロック原理の詳細な説明

ロック

ロック部分では、現在のオペレータを区別するための ID が生成されます。

安全のため、デフォルトのタイムアウトも設定されています。

もちろん、これは発信者の使用コストを簡素化するためです。これを使用する場合、開発者はロックしたいキーのみに注意する必要があります。

もちろん、分散ロックを実装するメソッドに @DistributedLock をカプセル化するなど、ロック キーをさらに抽象化することもできます。これは後で時間があるときに拡張することができ、原理は難しくありません。

ロック解除

ロックを解除すると、現在のプロセスの保持フラグが取得されます。

現在のスレッドが保持している ID を使用してロックを解除します。

I演算子

Redis の操作を抽象化しました。なぜ抽象化したのでしょうか?

Redis サービスには多くの種類があるため、Redis シングル ポイント、クラスター、マスター スレーブ、センチネルなどがあります。

jedis、spring redisTemplate、codis、redisson など、接続されるクライアントは多数ある可能性があります。

後の拡張を容易にするために、ここでは操作を抽象化します。

インタフェース

インターフェースは次のように定義されます。

  1. パッケージcom.github.houbb.lock.redis.support.operator;
  2.  
  3. /**
  4. * Redisクライアント
  5. * @author binbin.hou
  6. * @0.0.1 以降
  7. */  
  8. 公共 インターフェースIOperator {
  9.  
  10. /**
  11. * 分散ロックを取得しようとする
  12. *
  13. * @param lockKey ロック
  14. * @param requestId リクエスト識別子
  15. * @param expireTimeMills 有効期限
  16. * @return 取得が成功したかどうか
  17. * @0.0.1 以降
  18. */  
  19. ブール型ロック(文字列 lockKey、文字列 requestId、 int expireTimeMills);
  20.  
  21. /**
  22. * ロック解除
  23. * @param lockKey ロックキー
  24. * @param requestId リクエスト識別子
  25. * @結果を返す
  26. * @0.0.1 以降
  27. */  
  28. ブール値のロック解除(文字列 lockKey、文字列 requestId);
  29.  
  30. }

ジェダイの実装

Jedis のシングルポイント バージョンを実装します。

  1. パッケージcom.github.houbb.lock.redis.support.operator.impl;
  2.  
  3. com.github.houbb.lock.redis.constant.LockRedisConstをインポートします
  4. com.github.houbb.lock.redis.support.operator.IOperatorをインポートします
  5. redis.clients.jedis.Jedis をインポートします
  6.  
  7. java.util.Collectionsをインポートします
  8.  
  9. /**
  10. * Redisクライアント
  11. * @author binbin.hou
  12. * @0.0.1 以降
  13. */  
  14. 公共  JedisOperatorクラスはIOperatorを実装します{
  15.  
  16. /**
  17. * ジェディスクライアント
  18. * @0.0.1 以降
  19. */  
  20. プライベート 最後のジェダイ、ジェダイ。
  21.  
  22. パブリックJedisOperator(Jedis jedis) {
  23. this .jedis = jedis;
  24. }
  25.  
  26. /**
  27. * 分散ロックを取得しようとする
  28. *
  29. * expireTimeMillsは、現在のプロセスがハングした場合でもロックを解除できることを保証します。
  30. *
  31. * requestIdは現在のプロセス(ロック保持者)がロック解除されていることを確認します
  32. *
  33. * @param lockKey ロック
  34. * @param requestId リクエスト識別子
  35. * @param expireTimeMills 有効期限
  36. * @return 取得が成功したかどうか
  37. * @0.0.1 以降
  38. */  
  39. @オーバーライド 
  40. 公共 ブール型ロック(文字列ロックキー、文字列リクエストID、 int有効期限ミル) {
  41. 文字列結果 = jedis.set(lockKey、requestId、LockRedisConst.SET_IF_NOT_EXIST、LockRedisConst.SET_WITH_EXPIRE_TIME、expireTimeMills);
  42. LockRedisConst.LOCK_SUCCESS.equals(結果)を返します
  43. }
  44.  
  45. /**
  46. * ロック解除
  47. *
  48. * (1) requestIdを使用して、自分が現在のロック保持者であることを確認する
  49. * (2)実行のアトミック性を保証するためにluaスクリプトを使用します。
  50. *
  51. * @param lockKey ロックキー
  52. * @param requestId リクエスト識別子
  53. * @結果を返す
  54. * @0.0.1 以降
  55. */  
  56. @オーバーライド 
  57. 公共 ブール値のロック解除(文字列ロックキー、文字列リクエストID) {
  58. 文字列スクリプト = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" ;
  59. オブジェクト結果 = jedis.eval(script、Collections.singletonList(lockKey)、Collections.singletonList(requestId));
  60. LockRedisConst.RELEASE_SUCCESS.equals(結果)を返します
  61. }
  62.  
  63. }

これが最も重要な部分です。

これらの数行のコードを過小評価しないでください。まだ注意すべき点はたくさんあります。

ロック

ロックを追加するときは、自分自身をロック保持者として識別するための requestId が含まれます。

SETNX はキーが存在しない場合にのみロックします。

ロックの解除に失敗するなどの異常な理由により、長期間のロック占有を回避するために、ロックの有効期限を設定します。

ロック解除

操作のアトミック性を保証するには、Lua スクリプトを使用します。

ロックの所有者であることを証明するには、requestId を渡します。

テスト検証

Mavenの紹介

  1. <依存関係>
  2. <groupId>com.github.houbb</groupId>
  3. <artifactId>ロックコア</artifactId>
  4. <バージョン> 0.0.1 </バージョン>
  5. </依存関係>

テストコード

  1. ジェディス jedis = new Jedis( "127.0.0.1" , 6379 );
  2. IOperator 演算子 =新しいJedisOperator(jedis);
  3.  
  4. // ロックを取得する 
  5. ILock ロック = LockRedisBs.newInstance().operator(operator).lock();
  6.  
  7. 試す{
  8. ブール型lockResult = lock.tryLock();
  9. System.out.println(lockResult);
  10. //ビジネス処理 
  11. }キャッチ(例外 e) {
  12. e.printStackTrace();
  13. 最後に
  14. ロックを解除します。
  15. }

まとめ

この時点で、Redis 分散ロックのシンプルなバージョンが実装されました。

もちろん、まだ改善の余地はたくさんあります。

(1)例えば、分散ロックにおけるGCによる問題を回避するために増分シーケンスを導入する

(2)Redisサーバとクライアントのさらなるサポート

(3)アノテーションスタイルのRedis分散ロックのサポート

<<:  Oracle から離れる傾向が強まっています。 AWSは30万のデータベースのクラウドへの移行を完了したと発表した

>>:  ITサービス管理の未来

推薦する

香港サーバー: zenlayer、30% 割引、1Gbps 香港 CN2 GIA 大帯域幅、月額 181 ドルから

世界的に有名なデータセンターであるZenlayerも香港に独自のデータセンターを持ち、デフォルトの帯...

組織効率化 - ニューノーマル時代の人材戦略No.1

2022年8月10日、Mokaは「新常態下における組織効率向上-人材戦略No.1」をテーマに、北京で...

アメリカの配車サービスであるUberは、どのようにして4年間で時価総額を180億ドルにまで成長させたのでしょうか?

海外メディアの報道によると、アメリカの配車サービス「ウーバー」の評価額はわずか4年でゼロから驚異の1...

budgetvm サーバー E3 シリーズ 初月 50% 割引

budgetvm のE3 シリーズのすべてのサーバーでは、最初の 1 か月間 50% オフの割引コー...

henghost: 超高速香港サーバー (PING 5MS 未満)、双方向 CN2+アジア太平洋 BGP 帯域幅、20M 帯域幅、無制限のトラフィック

私はSonderCloud Limitedを推薦したいと思います。同社は2010年から香港の自社デー...

Dynatrace がクラウド市場を制覇し、ソフトウェア インテリジェンスの新時代を創造

2018年7月18日、第5回Dynatrace Perform Greater Chinaユーザーカ...

WeChatモバイルインターネットビッグデータオープンプラットフォーム

「人間の行動の多くは、特定の統計法則に従います。この意味で、人間の行動の 93% は予測可能です」と...

#11.11# Zji:「専用サーバー」最大50%オフ、香港/韓国/日本/台湾、ステーションクラスター、高防御シリーズなどを含む。

プロフェッショナルな独立サーバーブランドであるzjiも、予定通り「ダブルイレブン」特別プロモーション...

ウェブサイト最適化のための強固な基盤を築く方法

ウェブサイトの最適化は、ほとんど労力をかけずに素晴らしい結果を達成できることで常に知られていますが、...

簡単な説明: SEOデータ分析機能の重要性

周知のとおり、検索エンジンのアルゴリズムは最近頻繁に更新されており、SEO 業界が直面している課題は...

ウェブページのランキングが突然消えた原因の分析

私は SEO の仕事でこのような問題に遭遇しました。新しいサイトが最初のうちは非常に高い順位にランク...

あなたのウェブサイトがBaiduによってペナルティを受けたことを示すいくつかの兆候とその後の解決策

情報量が膨大になった今日の世界では、検索エンジンはインターネットを閲覧するすべての人にとって非常に便...

Baiduの外部リンクツールから、Baiduが収集している外部リンクを確認できます。

数日前、私はGoogleウェブサイトコンソールとYoudaoを通じてアンカー外部リンクを照会する記事...

ショッピングガイドコミュニティゲームでの生存:大規模プラットフォームによってトラフィックがブロックされる

王玉佳5月30日、百度で「Mogujie」を検索すると、リンクはTaobaoのYitao売れ筋チャン...

Raksmart Japan VPSの簡単なレビュー、CN2+BGPハイブリッドで速度はかなり良い

中国でうまく機能する日本のVPSは比較的少なく、ほとんどがNTTなどのネットワークです。日本のcn2...