redis分散ロックの包括的な分析

序文

今日は分散ロックについて学びましょう。分散ロックはクラスター環境では一般的です。分散ロックは、在庫を差し引くシナリオなど、単一マシンのロックでは解決できない問題を解決するために使用されます。差し引かれるビジネスマシンが複数のユニットに展開されている場合、表示されます。売られ過ぎ現象(JAVAでの共通ロックと同期は両方ともスタンドアロンロックです)、この時点で分散ロックを導入する必要があります。

分散ロックには多くの実装がありますが、最も一般的なのはredisとzookeeperを使用することです。今日は、redisを介して分散ロックを実装しましょう。

分散ロック

Redis分散ロック関連のビデオの説明:学習ビデオ

Redisは分散ロックを実装しているので、redisを使用して実装する方法を検討することもできます。条件は次のとおりです。この操作は相互に排他的であり、相互排除の効果を実現できるredisの命令は1つだけです。つまり、setnx( k)命令、この命令の意味つまり、指定されたkが存在しない場合、実行は成功します。指定されたkがredisにすでに存在する場合、falseを返します。これにより、複数のマシン間の相互排除を完全に実現できます(例:既存の5台のマシン、最初のマシンsetnx( "lock")を実行した後、最初のマシンがこのキーを解放するまで、他のマシンはsetnx( "lock")の実行に失敗します。

コードデモ

在庫を差し引くために注文が作成されるシナリオを単純にシミュレートします

    @Autowired
    private RedisTemplate redisTemplate;
    
    private final String LOCKKEY="DistributedLock";
    
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //尝试创建锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
        if (flag){
            if (!StringUtils.isEmpty(goodsId)){
                Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                if (stock>0){
                    stock--;
                    redisTemplate.opsForValue().set(goodsId,stock);
                    //......创建订单
                    //释放锁
                    redisTemplate.delete(LOCKKEY);
                    return "创建订单成功";
                }else {
                    redisTemplate.delete(goodsId);
                    //释放锁
                    redisTemplate.delete(LOCKKEY);
                    return "库存不足";
                }
            }else {
                //释放锁
                redisTemplate.delete(LOCKKEY);
                return "库存不足";
            }
        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }

これで、分散ロックの最も単純なバージョンを実装しましたが、まだ多くの問題があります。以下では、これらの問題を段階的に解決し、最適化します。

問題を解く

問題1:プログラム例外によって引き起こされたロックが解放されない

問題の説明:ビジネスの実行中に異常な状況が発生し、ロックが解除されるまでコードが実行されない可能性が非常に高いです。この状況では、コードを改善する必要があります。コードは次のとおりです。

@SuppressWarnings("all")
@Controller
public class Lock {
    @Autowired
    private RedisTemplate redisTemplate;
    private final String LOCKKEY="DistributedLock";
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //尝试创建锁
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value");
        if (flag){
            try {
                if (!StringUtils.isEmpty(goodsId)){
                    Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                    if (stock>0){
                        stock--;
                        redisTemplate.opsForValue().set(goodsId,stock);
                        //......创建订单
                        return "创建订单成功";
                    }else {
                        redisTemplate.delete(goodsId);
                        return "库存不足";
                    }
                }else {
                    return "库存不足";
                }
            }finally {
                //释放锁,防止出现异常导致的锁无法释放场景
                redisTemplate.delete(LOCKKEY);
            }

        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }
}

Linuxバックエンドネットワークインフラストラクチャ開発に関する詳細情報を共有して、学習の原則に関する知識を強化します。学習資料の取得をクリックし、テクノロジースタックを改善し、Linux、Nginx、ZeroMQ、MySQL、 Redis、スレッドプール、MongoDB、ZKなどのコンテンツ知識を向上させます。 、Linuxカーネル、CDN、P2P、Epoll、Docker、TCP / IP、coroutine、DPDKなど。

問題2:ダウンタイムによって引き起こされたロックを解放できない

ロックが正常に追加されると、サーバーがダウンし、現時点ではロックを解放できません。したがって、ロックがダウンしても最終的にロックが解放されるように、ロックに有効期限を追加する必要があります。

  • 注:ロックの有効期限を追加する操作は、ロックを追加するアトミック操作である必要があります。そうしないと、ロックが追加されて有効期限が追加されようとしているときにサーバーがダウンします。

このアトミック操作がredisTemplateでサポートされている場合があります。例は、次のとおりです。

Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, "value",20L, TimeUnit.SECONDS);

質問3:スレッドがBスレッドのロックを解除します

このような状況も考えられます。Aスレッドがロックを追加した後、何らかの理由でビジネスコードが実行されていません。このとき、ロックは自動的に解放され、Bスレッドは正常にロックを取得します。Bスレッドが実行を終了する前に、Aスレッドfinallyコードブロックが実行されると、Bスレッドのロックが直接解除されます。

  • 解決策:スレッドごとに異なる値をランダムに生成し、リリースする前に値が同じかどうかを比較できます
    @PostMapping("/createOrder")
    public String createOrder(String goodsId){
        //生成value
        String value = UUID.randomUUID().toString().replace("-", "");
        //尝试创建锁,并设置过期时间
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(LOCKKEY, value,20L, TimeUnit.SECONDS);
        if (flag){
            try {
                if (!StringUtils.isEmpty(goodsId)){
                    Integer stock = (Integer)redisTemplate.opsForValue().get(goodsId);
                    if (stock>0){
                        stock--;
                        redisTemplate.opsForValue().set(goodsId,stock);
                        //......创建订单
                        return "创建订单成功";
                    }else {
                        redisTemplate.delete(goodsId);
                        return "库存不足";
                    }
                }else {
                    return "库存不足";
                }
            }finally {
                //释放锁,防止出现异常导致的锁无法释放场景
                Object o = redisTemplate.opsForValue().get(LOCKKEY);
                if (o!=null){
                    String redisValue=(String)o;
                    //当且仅当redis中的value和当前线程value相同时,释放锁
                    if (value.equals(redisValue)){
                        redisTemplate.delete(LOCKKEY);
                    }
                }
            }

        }else {
            //对于没有获取到锁的用户,给它返回一个提示信息,让他重试即可
            return "系统繁忙,请稍后重试";
        }
    }

問題4:ロックを解除する操作がアトミックではない

今のコードでは、ロック操作の解放はアトミックではなく、クエリを実行してから削除する操作であることがわかります。これにも問題があります。つまり、Aスレッドがクエリを実行して次の値を取得します。ロックであり、値もAと同じです。スレッドの値はまったく同じです。ロックが解放されようとすると、ロックは自動的に期限切れになり、Bスレッドは正常にロックされ、AはBのロックを再び解放します。

解決:

  • 最初の方法:Luaスクリプトを使用して、ロックを解放する操作をアトミックにします。
 		//释放锁,防止出现异常导致的锁无法释放场景
                //lua脚本保证原子性
                String script="if redis.call(‘get’,KEYS[1]) == ARGV[1]" +
                      "then" +
                      "    return redis.call(‘del’,KEYS[1])" +
                      "else" +
                      "    return 0" +
                      "end";
                Object eval = jedis.eval(script, Collections.singletonList(LOCKKEY),
                        Collections.singletonList(value));
                if ("0".equals(eval.toString())){
                    System.out.println("删除失败");
                }else {
                    System.out.println("删除锁成功");
                }
                jedis.close();
  • 2番目の方法:redisでトランザクションを導入します。キーを監視してトランザクションを開始できるwatchコマンドがあります。監視されたキーが他のスレッドによって操作されている場合、キーが実行されているため、送信は失敗します。操作
//释放锁,防止出现异常导致的锁无法释放场景
//开启手动提交事务
redisTemplate.setEnableTransactionSupport(true);
//监听锁
redisTemplate.watch(LOCKKEY);
//开启事务
redisTemplate.multi();
if (value.equals(redisTemplate.opsForValue().get(LOCKKEY))){
          redisTemplate.delete(LOCKKEY);
}
//提交事务(如果在监听到现在提交这段时间,锁被其他线程占用,那么删除锁操作不会执行,否则删除锁成功)
redisTemplate.exec();   
//取消监听
redisTemplate.unwatch();

質問5:マスタースレーブアーキテクチャでの自動ロックの失敗と問題

  • 自動ロック障害の問題

質問3と質問4を今すぐ確認できます。最終的な分析では、ロックが自動的に期限切れになり、他のスレッドがロックを占有するためです。

ロックの自動有効期限は、ロックの存続時間がビジネスの実行時間よりも短いためです。それを解決するにはどうすればよいですか?実際、別のスレッドを開始して、ロックの存続時間を定期的に延長し、ロックが削除されたときにスレッドを停止することができます。

  • マスタースレーブアーキテクチャでの問題

この問題は主に、マスターライブラリとスレーブライブラリが2つあり、ロックがマスターライブラリに正常に追加され、データがスレーブライブラリに同期されていないことを前提としています。このとき、マスターライブラリはダウンしています。スレーブライブラリが新しいマスターライブラリになり、スレッドが来ます。新しいメインライブラリにロックを追加するために、追加は成功します。この時点で、2つのスレッドが同時にロックを正常に追加しました。方法それを解決しますか?スレーブライブラリの遅延再起動を設定し、ロックの期限が切れたときにスレーブライブラリを新しいマスターライブラリにすることができます。

総括する

redisを介して分散ロックを実装する際に注意すべき点がたくさんあることがわかりますが、すべての注意点を書いたわけではないので、自分で分散ロックを実装するのはまだ非常に難しいので、実際に市販されている分散ロックを使用すれば十分です。たとえば、よく知られているRedissionフレームワークは、redis分散ロックの実現に役立ちます。使用方法も非常に簡単です。直接getlockしてから、ロックしてロックを解除するだけです。

おすすめ

転載: blog.csdn.net/Linuxhus/article/details/115144974