(Redisの記事)超微細なRedisson分散ロックロックのソースコード分析

序文

redissonはRedisクライアントの優れたJavaバージョンであり、ますます多くの分散シナリオで多くの同時実行セキュリティの問題を解決します。この記事では、redisson分散ロックのソースコード実装のみを分析します。

分散シナリオでの通常のロックの不十分さ

通常のモノリシックシナリオでの同時実行の問題の場合、ロック操作にSynchronizedまたはReentrantLockを選択して、アトミック操作を保証できます。分散アーキテクチャまたはクラスターアーキテクチャで発生する場合、このjvmプロセスレベルのロックでは保証できません。

シングルロックsituation.png
図に示すように、クライアント要求は負荷分散メカニズムの下でさまざまなクラスターノードに分散され、パークロック操作は単一のマシンのみをブロックします

分散ソリューション

分散状況.pngこの問題を解決するには、中間バッファーゾーンを作成するだけで済みます。次に、データベース、zookeeperなどのミドルウェアを使用して解決する必要があります。redisはすべて分散ロックの実装に役立ちます。

Redisson分散ロックの使用法

依存関係をインポートする

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>
复制代码

構成の書き込み

package redisson;

import org.redisson.Redisson;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * DATE: 2022/4/13
 * @author 码下客
 */
@Configuration
public class RedissonConfig {

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}
复制代码

サンプルコード

package redisson;

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * DATE: 2022/4/13
 * @author: 码下客
 */
@Service
public class RedissonLockService {

    @Autowired
    private Redisson redisson;

    public void setLock(){
        String lockKey = "redisKey";
        //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        redissonLock.lock();
        try {
            /**
             * 有并发安全问题的业务逻辑
             */
        } finally {
            redissonLock.unlock();
        }
    }
}
复制代码

redissonロックの使用は、依然として非常に簡単です。同時実行性のセキュリティの問題があるビジネスにロックを追加し、最後にロックを解除して、異常なデッドロックを防ぐだけです。

RedissonLockソースコード分析

ソースコードを分析する前に、redis分散ロックを自分で実装するためにどのような問題が必要になるかを考えてみましょう。

一般的に、一般的に使用されるredisロックコマンドはsetnxです。その後、setnxを使用してマークを付けてロックした後、システムが突然ハングした場合、ロックはデッドロックになりますか?さらに、タイムアウト期間、このタイムアウト期間設定は適切ですか?設定が短すぎるため、Aビジネスが終了する前に自動的にロックが解除されます。このとき、別のスレッドがBビジネスを実行してロック操作を実行します。同時に、Aビジネスが終了し、Bビジネスロックが解除されました。現時点では、ビジネスBの実行中にロックはありません。スレッドの安全性の問題が再び発生しますか?ロックする前に各スレッドにuuidを与えてから、ロックを解除してuuidが同じかどうかを判断します。 、ロックを解除してuuidを判別することはアトミックな操作ではないため、現時点では問題を完全に回避することはできません。これらの質問をredissonlockに持っていき、答えを見つけてください。

ロック

ソースコードRedissonLock.javaに移動します

@Override
public void lock() {
    try {
        lockInterruptibly();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
复制代码

lock方法这是我们调用的加锁入口持续定位lockInterruptibly()->lockInterruptibly(-1, null);

    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }
复制代码

这个方法必然是我们的核心方法,我们逐步去分析:

long threadId = Thread.currentThread().getId();
复制代码

首先获取到了一个线程id,然后去调用了tryAcquire()方法,见名思意 尝试获得锁,那么下面看看tryAcquire()的业务逻辑

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }

            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}
复制代码

leaseTime != -1 的逻辑处理先不看,走我们的主干线,调用了一个tryLockInnerAsync()方法 异步去尝试加锁,首先看第一个参数 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout() 指向

private long lockWatchdogTimeout = 30 * 1000;
复制代码

含义是看门狗锁的过期时间,第二个参数 TimeUnit.MILLISECONDS 时间单位,第三个参数 threadId 线程id, 第四个参数 RedisCommands.EVAL_LONG 等等执行lua脚本要使用。接下来去详细看看这个方法。

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "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]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
复制代码

主要就是封装的这一大段lua脚本,我们来剖析这lua脚本之前先了解lua脚本的基本语义。如图上命令

redis.call('exists', KEYS[1]) == 0)
复制代码

redis.call你可以看作是lua脚本下调用redis命令的一个方法,'exists'是具体的某个方法也就是对应的redis元素生命令,KEY[1]呢是从Collections.singletonList(getName())这个集合中取值,getName()是一开始创建锁的key值,那么我们就得出第一个命令(redis.call('exists', KEYS[1]) == 0) 的含义是判断这个key存不存在redis中。剩下的语义同理,ARGV[1]对应 internalLockLeaseTime 代表超时时间, ARGV[2]对应 getLockName(threadId) 含义是uuid拼接线程id后的字符串。那么第一个if分支的lua脚本含义就是判断key是否存在redis中,如果不存在我就设置这个key为hash并且参数为一个uuid+线程id,然后又将这个key设置了一个过期时间返回null, 接下来我们先走返回null 设置成功的代码逻辑。

再次回到tryAcquireOnceAsync()方法

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
    @Override
    public void operationComplete(Future<Long> future) throws Exception {
        if (!future.isSuccess()) {
            return;
        }

        Long ttlRemaining = future.getNow();
        // lock acquired
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    }
});
复制代码

刚刚执行的tryLockInnerAsync()返回值是一个future任务(这个不懂的可以去了解一下并发编程的知识),下面是调用了一个监听器,也就是说他监听tryLockInnerAsync()这个异步任务,执行完成就会去回调operationComplete()方法,这个方法内逻辑首先是判断这个任务执行是否成功,然后又调了一个getNow(),这个就是取这个任务的返回值,刚刚我们分析那段lua语义也得到设置成功返回是null,也就走到了scheduleExpirationRenewal()这个锁续命逻辑,给我们刚刚设置的30秒到期时间去定时续命。

private void scheduleExpirationRenewal(final long threadId) {
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            
            RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                    "end; " +
                    "return 0;",
                      Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
            
            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can't update lock " + getName() + " expiration", future.cause());
                        return;
                    }
                    
                    if (future.getNow()) {
                        // reschedule itself
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
        task.cancel();
    }
}
复制代码

我们直接看他的主干线逻辑,new TimerTask()这里是又搞了一个任务,他有两个参数internalLockLeaseTime / 3 和TimeUnit.MILLISECONDS, 代表10秒之后去执行这个任务(回顾--- 当前方法是监听设置锁成功后执行的 那么也就是说 我当前的new TimerTask()任务是会在设置成功后10秒执行,这个key的过期时间还剩下20秒),然后去执行了一段lua脚本,大致意思就是去判断这个key是否还存在,存在的话去设置新的过期时间 返回值是1(redis 1 0 返回到代码中对应是boolean)。下面又一个addListener监听器去监听future任务,看到当续命成功时又去递归调用 scheduleExpirationRenewal()方法,通过递归的方式来达到类似定时任务的效果。
到这里加锁成功的核心逻辑也就读完了,在回去看一下加锁失败的逻辑。

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "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]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
复制代码

加锁失败是执行return redis.call('pttl', KEYS[1]); 这里是返回了这把锁的剩余过期时间,继续回到 lockInterruptibly()方法

    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }
复制代码

Long ttl = tryAcquire(leaseTime, unit, threadId); 尝试加锁这个逻辑刚刚读完我们知道 ttl是null 说明加锁失败 ttl有值则是加锁失败获取这把锁剩下的过期时间。subscribe()是一个订阅功能,给当前加锁失败的线程去订阅一个channel,有订阅肯定有通知稍后去讲通知在哪里,以及通知的作用。我们先看这个while(true),又去尝试加锁刷新这个ttl的时间, 分析ttl >= 0的逻辑,getLatch()这个返回值

public Semaphore getLatch() {
    return latch;
}
复制代码

これはセマフォセマフォです。このセマフォを取得した後、tryAcquire()メソッドが呼び出され、パラメータはttlだけです。これは、許可を取得するためにttl秒待機することを意味します。ttlが10秒であるとすると、このメソッドはブロックされ、ここで待機します。 10秒間ループします。ロックを続行するには、これをループします。
閉塞があるので、目覚めがあり、どのような状況で目覚めますか。シナリオを想定すると、ttlが20秒を返すと、ロックを保持しているスレッドのロックが解除され、ロックの取得を待機しているスレッドは、20秒待機し続けるのではなく、すぐにロックを試行することを検知する必要があります。ロックを解除したとき。

    @Override
    public void unlock() {
        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + Thread.currentThread().getId());
        }
        if (opStatus) {
            cancelExpirationRenewal();
        }

//        Future<Void> future = unlockAsync();
//        future.awaitUninterruptibly();
//        if (future.isSuccess()) {
//            return;
//        }
//        if (future.cause() instanceof IllegalMonitorStateException) {
//            throw (IllegalMonitorStateException)future.cause();
//        }
//        throw commandExecutor.convertException(future);
    }
复制代码

ソースコードのロックを解除するには、unlockInnerAsync()の非同期メソッドのみを確​​認します。

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}
复制代码

luaのセマンティクスの分析を続けると、redisのpublishコマンドを呼び出すredis.call('publish'、KEYS [2]、ARGV [1])のコマンドがあります。ロックの時間がチャネルにサブスクライブしました。これは、このチャネルにメッセージを送信し、チャネルにサブスクライブしているスレッドに、ロックを解除したことを通知することです。次に、消費メッセージのソースコードに移動します。LockPubSub.classの下にonMessageがあります

@Override
protected void onMessage(RedissonLockEntry value, Long message) {
    if (message.equals(unlockMessage)) {
        value.getLatch().release();

        while (true) {
            Runnable runnableToExecute = null;
            synchronized (value) {
                Runnable runnable = value.getListeners().poll();
                if (runnable != null) {
                    if (value.getLatch().tryAcquire()) {
                        runnableToExecute = runnable;
                    } else {
                        value.addListener(runnable);
                    }
                }
            }
            
            if (runnableToExecute != null) {
                runnableToExecute.run();
            } else {
                return;
            }
        }
    }
}
复制代码

value.getLatch()。release();セマフォを取得してライセンスを解放するかどうかに関係なく、ロック内のtryAcquire()が解放され、ループ内で再度ロックを試みて、閉ループを形成します。

要約:

ロックのロックが成功した場合、アトミック性はluaスクリプトによって保証されます。タイムアウト時間の制御できない要因により、ウォッチドッグメカニズムを使用してタイムアウト時間を再帰的に遅延させます。ロックが失敗すると、パブリッシュおよびサブスクライブします。 redisの機能を使用して、最初にサブスクライブします。スレッドチャネルをロックしてから、ループに移動してロックを試み、ループプロセスでセマフォの特性を使用して、ループを遅延させ、ウェイクアップと再ロックの待機をブロックします。 。
Unlockは、チャネルへのメッセージの送信を完了し、セマフォにウェイクアップの許可を解放させます。

この記事は少し抽象的です。Redissonのソースコードをダウンロードして、この記事と一緒に見るのが最善です。

おすすめ

転載: juejin.im/post/7086405088608157726