redisに基づく分散ロックコード例のJava実装

なぜそのような要求があるのですか?

たとえば、単純なユーザー操作、ユーザーの状態を変更するスレッド、最初にメモリ内のユーザーの状態を読み取り、次にメモリ内で変更してから、データベースに保存します。シングルスレッドでは、これは問題ありません。ただし、マルチスレッドでは、読み取り、変更、書き込みが3つの操作であり、アトミック操作(成功または失敗を同時に行う)ではないため、マルチスレッドではデータセキュリティの問題が発生します。

この場合、分散ロックを使用して、プログラムの同時実行を制限できます。

実現のアイデア:

これは、他のスレッドが入って動作するときに、誰かが席を占有しているのを見つけた場合、それらはあきらめるか、後で再試行することを意味します。

プレースホルダーの実現:

これは、redisのsetnxコマンドによって実装されます。redisコマンドについては、私のブログhttps://www.cnblogs.com/javazl/p/12657280.htmlを参照してください。デフォルトのsetコマンドは、値を保存することです。キーが存在し、set isキーの値を上書きしますが、setnxは上書きしません。キーがない場合は、setnxが最初に入力され、代わりになります。キーが存在する場合、他のsetnxは入力できません。最初の実行が完了するまで待ち、delコマンドでシートを解放します。

コード:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
     //setnx的返回值为long类型
      if (setnx == 1) {
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        //释放资源
         jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

上記のコードでは、分散ロックの単純な実装ですが、問題があります。つまり、占領後に解放する前に電話を切った場合です。その後、このスレッドは解放されません。つまり、delコマンドは呼び出されず、後続のすべての要求はここでブロックされ、ロックはデッドロックになります。したがって、ここで最適化する必要があります。

最適化の方法は、有効期限を追加して、一定期間後にロックを解放できるようにすることです。

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
      if (setnx == 1) {
        //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }

この処理の後、ロックが正常に解放されることを確認できます。ただし、ロックの取得と有効期限の設定中にサーバーがハングアップした場合、新しい問題が発生します。これは、ロックの取得、つまりsetnxと有効期限の設定が2つの操作であり、アトミックではなく、同時。このロックは永久に占有され、解放できず、デッドロックになります。それで、それをどのように解決するのですか?

redis2.8以降、setnxとexpirekeを1つのコマンドで一緒に実行でき、2つの操作が1つになり、この問題が解決されます。

最適化された実現:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
     //将两个操作合并成一个,nx就是setnx,ex就是expire
      String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
     //操作结果为okhuo或者error
      if (set !=null && "OK".equals(set)) {
     //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl);
        String name = jedis.get("name");
        System.out.println(name);
      //释放资源
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

有効期限で最適化した後、デッドロックの問題は解決されましたが、新しい問題、つまりタイムアウトの問題が発生します。

例:実行するビジネスに時間がかかる場合、混乱する可能性があります。ローカルスレッドがロックを取得すると、ビジネスコードの実行を開始しますが、ビジネスコードには時間がかかります。有効期限が3の場合このように、ロックは早期に解放され、2番目のスレッドがロックを取得して実行を開始します。実行が2秒に達すると、最初のロックも実行されます。このとき、最初のスレッドは2番目のスレッドのロックを解放し、3番目のスレッドは引き続きロックを取得して実行します。 2番目のスレッドが実行された後、ロックは事前に解放され、ループはスレッドの混乱を引き起こし続けます。

次に、2つの主な解決策があります

時間のかかる操作は避けてください。
ロックを処理するには、ロックの値に乱数またはランダムな文字列を設定し、解放されるたびに値の値を判断します。解放されている場合は解放し、そうでない場合は解放しません。たとえば、最初のスレッドが入ったときに、ロックを取得する値が1であるとします。タイムアウトが発生すると、次のスレッドに入り、次のスレッドは次のように新しい値を取得します。

3. 2番目の場所を解放する前に、値を取得して比較します。1が3に等しくない場合、ロックは解放されません。
最初のタイプについては何も言うことはありませんが、2番目のタイプには問題があります。つまり、ロックを解放すると値がチェックされ、比較されてから解放されます。3つの操作があるため、アトミック性はありません。 。この操作を実行すると、表示されます。デッドロック。ここでは、Luaスクリプトを使用して処理できます。

Luaスクリプトの機能:

1.使いやすく、redisにはLuaスクリプトのサポートが組み込まれています。

2.Luaはredisサーバー上で複数のredisコマンドをアトミ​​ックに実行できます

3.ネットワーク上の理由により、redisのパフォーマンスに影響があります。したがって、Luaを使用すると、複数のコマンドを同時に実行できるため、ネットワークによるredisのパフォーマンスの問題を軽減できます。

redisでLuaスクリプトを使用する方法:

1. redisサーバーに書き込み、Javaビジネスでスクリプトを呼び出します

2. Javaで直接書き込むことができます。書き込んだ後、実行する必要がある場合は、スクリプトをredisに送信して毎回実行します。

Luaスクリプトを作成します。

//用redis.call调用一个redis命令,调的是get命令,这个key是从外面传进来的keyif redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作释放命令
  return redis.call("del",KEYS[1])
else
 return 0
end

LuaスクリプトのSHA1合計は次のとおりです。

cat lua / equal.lua | redis-cli -a root script load --pipe
script loadこのコマンドは、LuaスクリプトをRedisにキャッシュし、スクリプトコンテンツのSHA1チェックサムを返し、javaで呼び出されたときにSHA1を渡します。チェックサムはパラメータとして使用されるため、redisサーバーは実行するスクリプトを認識します。

次にJavaで書く

public static void main(String[] args) {
    Redis redis = new Redis();
    for (int i = 0; i < 2; i++) {
      redis.execute(jedis -> {
        //1.先获取一个随机字符串
        String value = UUID.randomUUID().toString();
        //2.获取锁
        String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
        //3.判断是否成功拿到锁
        if (k1 != null && "OK".equals(k1)) {
          //4. 具体的业务操作
          jedis.set("site", "zl");
          String site = jedis.get("site");
          System.out.println(site);
          //5.释放锁
          jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", 
Arrays.asList("k1"), Arrays.asList(value));
        } else {
          System.out.println("没拿到锁");
        }
      });
    }
  }
}

このようにして、デッドロックの問題が解決されます。

最新の2020年に収集されたいくつかの高頻度のインタビューの質問(すべてドキュメントにまとめられています)には、mysql、netty、spring、thread、spring cloud、jvm、ソースコード、アルゴリズム、その他の詳細な説明など、多くの乾物があります。詳細な学習計画、インタビュー質問の並べ替えなど。これらのコンテンツを取得する必要がある場合は、次のようなQを追加してください:11604713672

おすすめ

転載: blog.csdn.net/weixin_51495453/article/details/113936698