分散ロックにおける King のソリューション - 再議論



ここに画像の説明を挿入

5.1 分散ロック再分配機能の紹介

setnx に基づく分散ロックには次の問題があります。

リエントラント問題: リエントラント問題とは、ロックを取得したスレッドが同じロックのコード ブロックに再び入ることができることを意味します. リエントラント ロックの重要性はデッドロックを防ぐことです. たとえば、HashTable などのコードでは、彼の方法は synchronized を使用します修正しました。あるメソッドで別のメソッドを呼び出した場合、現時点でそれがリエントラントでない場合、デッドロックではないでしょうか。したがって、リエントラント ロックの主な重要性は、デッドロックを防ぐことです。同期ロックとロック ロックは両方ともリエントラントです。

Non-retryable : 現在のディストリビューションは 1 回しか試行できないことを意味します。スレッドがロックの取得に失敗した場合、スレッドは再度ロックの取得を試行できるはずであるというのが合理的な状況であると考えられます。

タイムアウト解除デッドロックを防ぐため、ロック時の有効期限を長くしていますが、滞留時間が長すぎると、他人のロックを誤って削除しないようにlua式を使用していますが、結局ロックされず、セキュリティリスク

マスター/スレーブの一貫性: Redis がマスター/スレーブ クラスターを提供する場合、クラスターにデータを書き込むときに、ホストはデータをスレーブに非同期的に同期する必要があります。同期が完了する前にホストがクラッシュすると、デッドロックが発生します。

ここに画像の説明を挿入

それで、リディションとは何ですか?

Redisson は、Redis に基づいて実装された Java インメモリ データ グリッド (In-Memory Data Grid) です。これは、一連の分散共通 Java オブジェクトを提供するだけでなく、さまざまな分散ロックの実装を含む多くの分散サービスも提供します。

Redission は分散ロック用のさまざまな機能を提供します

ここに画像の説明を挿入


5.2 分散ロック - 再ディスセッションのクイックスタート

依存関係をインポートします。

<dependency>
	<groupId>org.redisson</groupId>
	<artifactId>redisson</artifactId>
	<version>3.13.6</version>
</dependency>

Redisson クライアントを構成します。

@Configuration
public class RedissonConfig {
    
    

    @Bean
    public RedissonClient redissonClient(){
    
    
        // 配置
        Config config = new Config();
        //这里添加的单点的地址,也可以使用config.useClusterServer 添加集群地址
        config.useSingleServer().setAddress("redis://192.168.150.101:6379")
            .setPassword("123321");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

Redission の分散ロックの使用方法

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    
    
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
    
    
        try{
    
    
            System.out.println("执行业务");          
        }finally{
    
    
            //释放锁
            lock.unlock();
        }
        
    }
}

VoucherOrderServiceImpl で

RedissonClient を注入する

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
    
    
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
    
    
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
    
    
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
    
    
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
    
    
            return Result.fail("不允许重复下单");
        }
        try {
    
    
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
    
    
            //释放锁
            lock.unlock();
        }
 }

5.3 分散ロックの原理 - 再圧縮リエントラントロック

Lock ロックでは、下部の voaltile の状態変数を使用して再突入の状態を記録します。たとえば、現在ロックを保持している人がいない場合は state=0、誰かがロックを保持している場合は state =1、このロックを保持している人が再度このロックを保持すると、状態は +1 になります。同期の場合、C 言語コードにカウントが含まれます。原理は状態と同様で、これも重要です。 1 つのエントリの場合は 1、1 つのリリースの場合は -1 で、0 まで減少し、現在のロックが誰にも保持されていないことを示します。

再議論では、リエントラントロックもサポートします

分散ロックでは、ハッシュ構造を使用してロックを保存します。大きなキーはロックが存在するかどうかを示し、小さなキーは現在ロックを保持しているスレッドを示します。そこで、現在のロックを一緒に分析しましょう。lua 式

この場所には 3 つのパラメータがあります

KEYS[1] : ロック名

ARGV[1]: ロックの有効期限

ARGV[2]: id + ":" + threadId; ロックの小さなキー

存在する: データが存在するかどうかを判断します。 名前: ロックが存在するかどうか。==0 の場合、現在のロックが存在しないことを意味します。

redis.call('hset', KEYS[1], ARGV[2], 1);この時点で、redis へのデータの書き込みを開始し、それをハッシュ構造に書き込みます。

Lock{
    
    id + **":"** + threadId :  1

}

現在のロックが存在する場合、最初の条件が満たされていないため、次の判断が行われます。

redis.call('hexists', KEYS[1], ARGV[2]) == 1

このとき、大きな鍵と小さな鍵を使用して、現在のロックが自分のものであるかどうかを判断する必要があります。それが自分のものである場合は、次に進みます。

redis.call('hincrby', KEYS[1], ARGV[2], 1)

現在のロックの値に +1 を加算して、 redis.call('pexpire', KEYS[1], ARGV[1]); 有効期限を設定します。上記の 2 つの条件が満たされない場合は、現在のロックがロックの取得に失敗し、最終的に現在のロックの有効期限である pttl を返します。

前のソースコードを見ると、現在のメソッドの戻り値が null かどうかを判定し、null の場合は最初の 2 つの if に対応する条件に該当し、ロック取得を終了します。これは null ではない、つまり 3 番目の分岐が選択され、ソース コードで while(true) スピン ロックが実行されます。

"if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);"

ここに画像の説明を挿入

5.4 分散ロック - 再分割ロックの再試行と WatchDog メカニズム

説明: tryLock のソース コード分析とそのウォッチドッグ原理についてはコース内で説明しましたので、ここでは著者が lock() メソッドのソース コード分析を分析し、学習の過程でより多くの知識を習得できることを願っています。

ロックを取得するプロセスでは、現在のスレッドが取得され、tryAcquire によってロックが取得されます。ロック取得のロジックは前のロジックと同じです。

1. まず現在のロックが存在するかどうかを判断し、存在しない場合はロックを挿入し、null を返します。

2. 現在のロックが現在のスレッドに属しているかどうかを判断し、属している場合は null を返します。

したがって、戻り値が null の場合は、現在のバディがロックの取得を完了したか、再エントリが完了したことを意味しますが、上記の 2 つの条件が満たされない場合は、3 番目の条件に入り、戻り値はその有効期限になります。ロック、クラスメート 自分で少し下にスクロールすると、ロックを取得するために tryAcquire を再度実行するのにしばらく時間がかかることがわかります (本当です)

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
    
    
    return;
}

次に条件分岐があります。lock メソッドにはオーバーロードされたメソッドがあり、1 つはパラメータ付き、もう 1 つはパラメータなしです。パラメータ付きで渡された値が -1 の場合、パラメータが渡された場合は、leaseTime 自体になります。つまり、パラメータがが渡されると、leaseTime != -1 が入り、この時点でロックを取得します。ロックを取得するロジックは、前述の 3 つのロジックです。

if (leaseTime != -1) {
    
    
    return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}

受信時刻がない場合、この時点でロックも取得され、ロックされた時刻がデフォルトのウォッチドッグ時刻になります。 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()

ttlRemainingFuture.onComplete((ttlRemaining, e) この文は、上記のロック取得を監視することに相当します。つまり、上記のロック取得が完了した後、このメソッドが呼び出されます。呼び出しの具体的なロジックは、スレッドをオープンすることです。バックグラウンドで更新ロジック、つまりウォッチドッグ スレッドを実行します。

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
    
    
    if (e != null) {
    
    
        return;
    }

    // lock acquired
    if (ttlRemaining == null) {
    
    
        scheduleExpirationRenewal(threadId);
    }
});
return ttlRemainingFuture;

このロジックは更新ロジックです。commandExecutor.getConnectionManager().newTimeout() メソッドに注目してください。

メソッド( new TimerTask() {}、パラメータ 2、パラメータ 3)

これは次のことを指します: パラメーター 2 とパラメーター 3 を使用してパラメーター 1 の処理を​​いつ実行するかを記述します。現在の状況は次のとおりです: 10 秒後にパラメーター 1 の処理を​​実行します。

ロックの有効期限は 30 秒であるため、10 秒後、この時点で timeTask がトリガーされ、コントラクトを更新し、現在のロックを 30 秒に更新します。操作が成功すると、この時点で自分自身を再帰的に呼び出します。次に timeTask() を再度設定し、さらに 10 秒後に timerTask を再度設定して、ノンストップ更新を完了します。

それなら誰もが考えてみてください、私たちのスレッドがダウンしたら、彼は契約を更新するでしょうか?もちろん、そうではありません。誰も renewExpiration メソッドを呼び出さないため、このメソッドは時間の経過後に解放されます。

private void renewExpiration() {
    
    
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
    
    
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
    
    
        @Override
        public void run(Timeout timeout) throws Exception {
    
    
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
    
    
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
    
    
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
    
    
                if (e != null) {
    
    
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
    
    
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

5.5 分散ロック - 再分散ロックの MutiLock 原理

Redis の可用性を向上させるために、クラスターまたはマスター/スレーブを構築します。ここではマスター/スレーブを例に挙げます。

この時点で、マスターにコマンドを書き込み、マスターはデータをスレーブに同期します。ただし、マスターがスレーブにデータを書き込む時間がなかった場合、この時点でマスターはダウンしており、センチネルはマスターがマシンダウンしていることを発見し、マスターになるスレーブを選択しますが、この時点で実際には新しいマスターにはロック情報が存在せず、この時点でロック情報は失われています。

ここに画像の説明を挿入

この問題を解決するために、再ディスションは MutiLock ロックを提案しました。このロックでは、マスター/スレーブを使用しません。各ノードのステータスは同じです。このロックをロックするロジックは、各マスター クラスター ノードに書き込む必要があります。一般に、すべてのサーバーが正常に書き込まれた場合にのみ、ロックは成功します。現在ノードがダウンしていると仮定すると、ロックを取得しようとしたときに、ロックを取得できないノードがある限り、ロックとみなされません。これにより、ロックの信頼性が保証されます。

ここに画像の説明を挿入

では、MutiLock ロックの原理は何でしょうか? 説明するために絵を描きました

複数のロックを設定する場合、再分割により複数のロックがコレクションに追加され、while ループを使用してロックの取得が試行されますが、ロックの数 * 1500 ミリ秒を追加するのに必要な合計ロック時間がかかります。ロックが 3 つあると仮定すると、時間は 4500 ミリ秒です。この 4500 ミリ秒以内にすべてのロックが正常にロックされたと仮定すると、この時点でロックは成功したとみなされます。4500 ミリ秒以内にロックに失敗したスレッドがある場合、ロックは成功したとみなされます。再試行する。

ここに画像の説明を挿入



ここに画像の説明を挿入



おすすめ

転載: blog.csdn.net/m0_60915009/article/details/131979741