(Artigo Redis) Análise do código-fonte do bloqueio de bloqueio distribuído Redisson ultrafino

prefácio

redisson é uma excelente versão java do cliente Redis, que resolve muitos problemas de segurança de simultaneidade em cenários cada vez mais distribuídos.Este artigo analisa apenas a implementação do código-fonte do redisson distribuído lock.

Inadequação de bloqueios comuns em cenários distribuídos

No caso de problemas de simultaneidade em cenários monolíticos comuns, podemos escolher Synchronized ou ReentrantLock para operações de bloqueio para garantir operações atômicas. Quando ocorre em arquiteturas distribuídas ou em cluster, esse bloqueio em nível de processo jvm não pode nos trazer. Garantia.

Situação de bloqueio único.png
Conforme mostrado na figura, as solicitações do cliente serão distribuídas para diferentes nós do cluster sob o mecanismo de balanceamento de carga, e a operação de travamento do parque bloqueará apenas a máquina única

Solução distribuída

Situação distribuída.pngPrecisamos apenas criar uma zona de buffer intermediária para resolver esse problema, então precisamos usar middleware para resolvê-lo, como banco de dados, zookeeper, redis podem nos ajudar a implementar um bloqueio distribuído.

Uso de bloqueio distribuído Redison

importar dependências

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

configuração de gravação

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);
    }
}
复制代码

Código de amostra

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();
        }
    }
}
复制代码

O uso de bloqueios de redisson ainda é muito simples. Você só precisa adicionar bloqueios ao negócio com problemas de segurança de simultaneidade e, em seguida, desbloqueá-los finalmente para evitar impasses anormais.

Análise do código-fonte do Redisson Lock

Antes de analisar o código-fonte, vamos pensar em quais problemas teremos para implementar um bloqueio distribuído redis?

De um modo geral, nosso comando de bloqueio redis comumente usado é setnx, então se o sistema travar repentinamente após usar setnx para marcar e bloquear, o bloqueio se tornará um impasse? Além do período de tempo limite, esse período de tempo limite Quanto tempo é a configuração apropriada? A configuração é muito curto, e o bloqueio é liberado sozinho antes que o negócio A termine de ser executado. Neste momento, outro thread vem para executar o negócio B para realizar a operação de bloqueio. Ao mesmo tempo, o negócio A é concluído e o B o bloqueio de negócios é liberado. Neste momento, não há bloqueio durante a execução do negócio B. Os problemas de segurança de encadeamento ocorrerão novamente? Se você fornecer a cada encadeamento um uuid antes do bloqueio e, em seguida, liberar o bloqueio para determinar se o uuid é o mesmo , ele não evitará completamente o problema agora. , porque liberar o bloqueio e determinar o uuid não é uma operação atômica. Leve essas perguntas para redisson lock para encontrar respostas.

Trancar

Acesse o código-fonte RedisonLock.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;
}
复制代码

É um semáforo Semaphore. Após adquirir este semáforo, o método tryAcquire() é chamado e o parâmetro é apenas ttl, o que significa esperar ttl segundos para obter permissão. Supondo que ttl agora seja 10 segundos, este método irá bloquear e esperar aqui por 10 segundos. Faça um loop enquanto continua tentando bloquear.
Uma vez que há um bloqueio, haverá um despertar e em que circunstâncias ele será acordado. Assumindo um cenário, se ttl retornar 20s, o encadeamento que mantém o bloqueio agora está desbloqueado e o encadeamento esperando para adquirir o bloqueio deve sentir imediatamente para tentar bloquear em vez de continuar esperando por 20s, então ele deve estar em Ele acordará quando desbloqueado.

    @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);
    }
复制代码

Para desbloquear o código-fonte, analisamos apenas o método assíncrono de 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));

}
复制代码

Continuando a analisar a semântica de lua, existe um comando de redis.call('publish', KEYS[2], ARGV[1]), que chama o comando de publicação de redis. time of lock se inscreveu em um canal, this A instrução é enviar uma mensagem para este canal e dizer ao thread que se inscreve no canal que liberei o bloqueio agora, e vamos ao código fonte da mensagem de consumo. Há um onMessage em LockPubSub.class

@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(); Seja para adquirir o semáforo e liberar a licença, o tryAcquire() na trava será liberado e tentaremos travar novamente em um loop, formando assim um loop fechado.

Resumir:

No caso de bloqueio bem-sucedido no bloqueio, a atomicidade é garantida pelo script lua. Devido aos fatores incontroláveis ​​do tempo limite, o mecanismo de watchdog é usado para atrasar recursivamente o tempo limite. Quando o bloqueio falha, o método de publicação e assinatura A função de redis é usada para se inscrever primeiro. Bloqueie o canal de thread e, em seguida, vá para o loop para tentar bloquear e use as características do semáforo no processo de loop para atrasar o loop e bloquear esperando para acordar e bloquear novamente .
O desbloqueio realiza o envio de uma mensagem para o canal, fazendo com que o semáforo libere a permissão para acordar.

Este artigo é um pouco abstrato, é melhor baixar o código fonte do Redisson e assisti-lo junto com este artigo.

Acho que você gosta

Origin juejin.im/post/7086405088608157726
Recomendado
Clasificación