なぜそのような要求があるのですか?
たとえば、単純なユーザー操作、ユーザーの状態を変更するスレッド、最初にメモリ内のユーザーの状態を読み取り、次にメモリ内で変更してから、データベースに保存します。シングルスレッドでは、これは問題ありません。ただし、マルチスレッドでは、読み取り、変更、書き込みが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("没拿到锁");
}
});
}
}
}
このようにして、デッドロックの問題が解決されます。