Os 5 poços de bloqueios redis distribuídos são realmente grandes e profundos

1. Introdução

Recentemente, a frequência do projeto on-line é bastante alta.Depois de trabalhar horas extras por vários dias, o corpo fica um pouco sobrecarregado e o espírito fica um pouco fraco, mas o lado comercial está desesperadamente pressionado. Quando o cérebro está confuso, você não pode chamá-lo de código, pode chamá-lo diretamente Bug. Fiquei acordada a noite toda e escrevi uma que bugfoi repreendida.

Porque o shopping é para fazer negócios, a freqüentar as deduções estoque de mercadorias, Application Clusters é implantado, a fim de evitar o inventário causa concorrente 超买超卖e outras questões, o uso de redisbloqueio distribuído para ser controlado. Eu pensei que lock.tryLockseria bom adicionar um bloqueio ao código de inventário deduzido

    /**
     * @author xiaofu
     * @description 扣减库存
     * @date 2020/4/21 12:10
     */
   public String stockLock() {
        RLock lock = redissonClient.getLock("stockLock");
        try {
            /**
             * 获取锁
             */
            if (lock.tryLock(10, TimeUnit.SECONDS)) {
                /**
                 * 查询库存数
                 */
                Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stockCount"));
                /**
                 * 扣减库存
                 */
                if (stock > 0) {
                    stock = stock - 1;
                    stringRedisTemplate.opsForValue().set("stockCount", stock.toString());
                    LOGGER.info("库存扣减成功,剩余库存数量:{}", stock);
                } else {
                    LOGGER.info("库存不足~");
                }
            } else {
                LOGGER.info("未获取到锁业务结束..");
            }
        } catch (Exception e) {
            LOGGER.info("处理异常", e);
        } finally {
            lock.unlock();
        }
        return "ok";
  }

Como resultado, após a execução do código comercial, eu esqueci de liberar o bloqueio lock.unlock(), o que causou a redischeia do pool de threads, uma redisfalha em larga escala do serviço e a confusão da dedução dos dados do inventário. O líder foi repreendido.

Com o uso do redistempo de bloqueio mais longo, eu encontrei o redisbloqueio do poço a ser mais longe do que o esperado. Mesmo nas perguntas da entrevista, redisa taxa de aparência de bloqueios distribuídos é relativamente alta, como: "Quais problemas você encontrou com bloqueios?" E "Como resolvê-los?" Basicamente, todos eles são solicitados por um conjunto de traços.

Hoje, compartilharei meu redisdiário com um cadeado distribuído e algumas soluções, e encorajo você.

1. O bloqueio não é liberado

Este caso é um erro estúpido que eu cometi o erro no topo, devido à thread atual para adquirir rediso bloqueio, o bloqueio não é liberado em tempo após operações de processamento, resultando em outros segmentos vão sempre tentar adquirir o bloqueio de bloqueio, por exemplo: O Jediscliente será relatado como segue Mensagem de erro

redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool

redis线程池Não há threads ociosos para processar comandos do cliente.

A solução também é muito simples: desde que tenhamos cuidado, o thread que obteve o bloqueio liberará o bloqueio a tempo após o processamento dos negócios.Se o bloqueio não for reinserido, o thread poderá liberar a conexão atual sleeppor um período de tempo.

  public void lock() {
      while (true) {
          boolean flag = this.getLock(key);
          if (flag) {
                TODO .........
          } else {
                // 释放当前redis连接
                redis.close();
                // 休眠1000毫秒
                sleep(1000);
          }
        }
    }

Segundo, a trava de B foi liberada por A

Sabemos que Rediso princípio é atingir um bloqueio SETNXde comando. Quando keya ausência do keyvalor é definido value, o valor de retorno 1; se dado keyjá existe, SETNXsem qualquer ação, o valor de retorno 0.

SETNX key value

Vamos imaginar esta cena: A, Bdois segmentos tentam key myLockbloqueio, A线程para obter um bloqueio (se o bloqueio 3秒expira), B线程apenas esperando para tentar adquirir o bloqueio, este ponto não é errado.

Se neste momento a lógica de negócios que é demorado, o tempo de execução ultrapassou o redisbloqueio expirado o tempo, quando A线程o bloqueio é automaticamente liberada (suprimido key), B线程detecta myLockisso keynão existe, SETNXo comando também tem um bloqueio.

No entanto, A线程depois de executar a lógica de negócios no momento , o bloqueio ainda será liberado (excluído key), o que faz com que B线程o bloqueio seja A线程liberado.

Para evitar a situação acima, geralmente precisamos trazer nosso próprio valuevalor exclusivo para identificar quando cada encadeamento está bloqueado e liberar apenas o valuevalor especificado key; caso contrário, haverá uma bagunça ao liberar o bloqueio.

Três, tempo limite da transação do banco de dados

emm ~ Os redisbloqueios de bate-papo ainda estão envolvidos nas transações do banco de dados? Não se apresse em olhar para baixo, observe o seguinte código:

   @Transaction
   public void lock() {

        while (true) {
            boolean flag = this.getLock(key);
            if (flag) {
                insert();
            }
        }
    }

Adicione uma @Transactionobservação a este método para iniciar a transação, como lançar uma exceção no código para reverter, para saber que a transação do banco de dados tem um limite de tempo limite e não aguardará incondicionalmente uma operação de banco de dados demorada.

Por exemplo: analisamos um arquivo grande e armazenamos os dados no banco de dados.Se o tempo de execução for muito longo, fará com que a transação seja revertida automaticamente quando atingir o tempo limite.

Depois que você keynão adquirir o bloqueio por um longo tempo, e o bloqueio de aquisição estiver 等待的时间muito além da transação do banco de dados 超时时间, o programa reportará uma exceção.

Geralmente, para resolver esse problema, precisamos alterar a transação do banco de dados para confirmar e reverter manualmente a transação.

    @Autowired
    DataSourceTransactionManager dataSourceTransactionManager;

    @Transaction
    public void lock() {
        //手动开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
        try {
            while (true) {
                boolean flag = this.getLock(key);
                if (flag) {
                    insert();
                    //手动提交事务
                    dataSourceTransactionManager.commit(transactionStatus);
                }
            }
        } catch (Exception e) {
            //手动回滚事务
            dataSourceTransactionManager.rollback(transactionStatus);
        }
    }

4. O bloqueio expirou e o negócio não foi executado

Essa situação é semelhante à segunda mencionada acima, mas a solução é um pouco diferente.

É o mesmo caso em que o redisbloqueio distribuído expira e a lógica de negócios não é concluída, mas há outra maneira de pensar sobre o problema: não seria resolvido se o redistempo de expiração do bloqueio fosse maior?

Ainda existe um problema. Podemos redisaumentar manualmente o tempo de expiração do bloqueio quando o bloqueamos. Mas por quanto tempo esse tempo é adequado? O tempo de execução da lógica de negócios é incontrolável e, se for ajustado por muito tempo, afetará o desempenho da operação.

Seria bom se o redistempo de expiração do bloqueio pudesse ser renovado automaticamente.

Para solucionar esse problema, usamos o rediscliente redisson, redissonque resolve redisalguns problemas difíceis em um ambiente distribuído , cujo objetivo é permitir que os usuários reduzam Redissua atenção e gastem mais energia no processamento da lógica de negócios.

redissonO bloqueio distribuído é bem encapsulado, basta chamá-lo API.

  RLock lock = redissonClient.getLock("stockLock");

redissonApós o bloqueio ser adicionado com êxito, uma tarefa agendada será registrada para monitorar o bloqueio, e o bloqueio será verificado a cada 10 segundos.Se o bloqueio ainda estiver em espera, o bloqueio será 过期时间renovado. O tempo de expiração padrão é 30 segundos. Este mecanismo também é chamado: " 看门狗", o nome. . .

Exemplo : se o tempo de bloqueio for 30 segundos, verifique uma vez após 10 segundos, depois que o serviço de bloqueio não for executado, ele será renovado e o tempo de expiração do bloqueio será redefinido para 30 segundos novamente.

Pela análise a seguir redissonele pode ser encontrado no código fonte para conseguir, se é 加锁, 解锁, 续约é o cliente de alguma lógica de negócios complexos, encapsulando o Luaroteiro enviado para redisassegurar a implementação desta lógica de negócios complexos 原子性.


@Slf4j
@Service
public class RedisDistributionLockPlus {

    /**
     * 加锁超时时间,单位毫秒, 即:加锁时间内执行完操作,如果未完成会有并发现象
     */
    private static final long DEFAULT_LOCK_TIMEOUT = 30;

    private static final long TIME_SECONDS_FIVE = 5 ;

    /**
     * 每个key的过期时间 {@link LockContent}
     */
    private Map<String, LockContent> lockContentMap = new ConcurrentHashMap<>(512);

    /**
     * redis执行成功的返回
     */
    private static final Long EXEC_SUCCESS = 1L;

    /**
     * 获取锁lua脚本, k1:获锁key, k2:续约耗时key, arg1:requestId,arg2:超时时间
     */
    private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[2]) == 1 then ARGV[2] = math.floor(redis.call('get', KEYS[2]) + 10) end " +
            "if redis.call('exists', KEYS[1]) == 0 then " +
               "local t = redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2]) " +
               "for k, v in pairs(t) do " +
                 "if v == 'OK' then return tonumber(ARGV[2]) end " +
               "end " +
            "return 0 end";

    /**
     * 释放锁lua脚本, k1:获锁key, k2:续约耗时key, arg1:requestId,arg2:业务耗时 arg3: 业务开始设置的timeout
     */
    private static final String UNLOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "local ctime = tonumber(ARGV[2]) " +
            "local biz_timeout = tonumber(ARGV[3]) " +
            "if ctime > 0 then  " +
               "if redis.call('exists', KEYS[2]) == 1 then " +
                   "local avg_time = redis.call('get', KEYS[2]) " +
                   "avg_time = (tonumber(avg_time) * 8 + ctime * 2)/10 " +
                   "if avg_time >= biz_timeout - 5 then redis.call('set', KEYS[2], avg_time, 'EX', 24*60*60) " +
                   "else redis.call('del', KEYS[2]) end " +
               "elseif ctime > biz_timeout -5 then redis.call('set', KEYS[2], ARGV[2], 'EX', 24*60*60) end " +
            "end " +
            "return redis.call('del', KEYS[1]) " +
            "else return 0 end";
    /**
     * 续约lua脚本
     */
    private static final String RENEW_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";

    private final StringRedisTemplate redisTemplate;

    public RedisDistributionLockPlus(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        ScheduleTask task = new ScheduleTask(this, lockContentMap);
        // 启动定时任务
        ScheduleExecutor.schedule(task, 1, 1, TimeUnit.SECONDS);
    }

    /**
     * 加锁
     * 取到锁加锁,取不到锁一直等待知道获得锁
     *
     * @param lockKey
     * @param requestId 全局唯一
     * @param expire   锁过期时间, 单位秒
     * @return
     */
    public boolean lock(String lockKey, String requestId, long expire) {
        log.info("开始执行加锁, lockKey ={}, requestId={}", lockKey, requestId);
        for (; ; ) {
            // 判断是否已经有线程持有锁,减少redis的压力
            LockContent lockContentOld = lockContentMap.get(lockKey);
            boolean unLocked = null == lockContentOld;
            // 如果没有被锁,就获取锁
            if (unLocked) {
                long startTime = System.currentTimeMillis();
                // 计算超时时间
                long bizExpire = expire == 0L ? DEFAULT_LOCK_TIMEOUT : expire;
                String lockKeyRenew = lockKey + "_renew";

                RedisScript<Long> script = RedisScript.of(LOCK_SCRIPT, Long.class);
                List<String> keys = new ArrayList<>();
                keys.add(lockKey);
                keys.add(lockKeyRenew);
                Long lockExpire = redisTemplate.execute(script, keys, requestId, Long.toString(bizExpire));
                if (null != lockExpire && lockExpire > 0) {
                    // 将锁放入map
                    LockContent lockContent = new LockContent();
                    lockContent.setStartTime(startTime);
                    lockContent.setLockExpire(lockExpire);
                    lockContent.setExpireTime(startTime + lockExpire * 1000);
                    lockContent.setRequestId(requestId);
                    lockContent.setThread(Thread.currentThread());
                    lockContent.setBizExpire(bizExpire);
                    lockContent.setLockCount(1);
                    lockContentMap.put(lockKey, lockContent);
                    log.info("加锁成功, lockKey ={}, requestId={}", lockKey, requestId);
                    return true;
                }
            }
            // 重复获取锁,在线程池中由于线程复用,线程相等并不能确定是该线程的锁
            if (Thread.currentThread() == lockContentOld.getThread()
                      && requestId.equals(lockContentOld.getRequestId())){
                // 计数 +1
                lockContentOld.setLockCount(lockContentOld.getLockCount()+1);
                return true;
            }

            // 如果被锁或获取锁失败,则等待100毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                // 这里用lombok 有问题
                log.error("获取redis 锁失败, lockKey ={}, requestId={}", lockKey, requestId, e);
                return false;
            }
        }
    }

    /**
     * 解锁
     *
     * @param lockKey
     * @param lockValue
     */
    public boolean unlock(String lockKey, String lockValue) {
        String lockKeyRenew = lockKey + "_renew";
        LockContent lockContent = lockContentMap.get(lockKey);

        long consumeTime;
        if (null == lockContent) {
            consumeTime = 0L;
        } else if (lockValue.equals(lockContent.getRequestId())) {
            int lockCount = lockContent.getLockCount();
            // 每次释放锁, 计数 -1,减到0时删除redis上的key
            if (--lockCount > 0) {
                lockContent.setLockCount(lockCount);
                return false;
            }
            consumeTime = (System.currentTimeMillis() - lockContent.getStartTime()) / 1000;
        } else {
            log.info("释放锁失败,不是自己的锁。");
            return false;
        }

        // 删除已完成key,先删除本地缓存,减少redis压力, 分布式锁,只有一个,所以这里不加锁
        lockContentMap.remove(lockKey);

        RedisScript<Long> script = RedisScript.of(UNLOCK_SCRIPT, Long.class);
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);
        keys.add(lockKeyRenew);

        Long result = redisTemplate.execute(script, keys, lockValue, Long.toString(consumeTime),
                Long.toString(lockContent.getBizExpire()));
        return EXEC_SUCCESS.equals(result);

    }

    /**
     * 续约
     *
     * @param lockKey
     * @param lockContent
     * @return true:续约成功,false:续约失败(1、续约期间执行完成,锁被释放 2、不是自己的锁,3、续约期间锁过期了(未解决))
     */
    public boolean renew(String lockKey, LockContent lockContent) {

        // 检测执行业务线程的状态
        Thread.State state = lockContent.getThread().getState();
        if (Thread.State.TERMINATED == state) {
            log.info("执行业务的线程已终止,不再续约 lockKey ={}, lockContent={}", lockKey, lockContent);
            return false;
        }

        String requestId = lockContent.getRequestId();
        long timeOut = (lockContent.getExpireTime() - lockContent.getStartTime()) / 1000;

        RedisScript<Long> script = RedisScript.of(RENEW_SCRIPT, Long.class);
        List<String> keys = new ArrayList<>();
        keys.add(lockKey);

        Long result = redisTemplate.execute(script, keys, requestId, Long.toString(timeOut));
        log.info("续约结果,True成功,False失败 lockKey ={}, result={}", lockKey, EXEC_SUCCESS.equals(result));
        return EXEC_SUCCESS.equals(result);
    }

    static class ScheduleExecutor {

        public static void schedule(ScheduleTask task, long initialDelay, long period, TimeUnit unit) {
            long delay = unit.toMillis(initialDelay);
            long period_ = unit.toMillis(period);
            // 定时执行
            new Timer("Lock-Renew-Task").schedule(task, delay, period_);
        }
    }

    static class ScheduleTask extends TimerTask {

        private final RedisDistributionLockPlus redisDistributionLock;
        private final Map<String, LockContent> lockContentMap;

        public ScheduleTask(RedisDistributionLockPlus redisDistributionLock, Map<String, LockContent> lockContentMap) {
            this.redisDistributionLock = redisDistributionLock;
            this.lockContentMap = lockContentMap;
        }

        @Override
        public void run() {
            if (lockContentMap.isEmpty()) {
                return;
            }
            Set<Map.Entry<String, LockContent>> entries = lockContentMap.entrySet();
            for (Map.Entry<String, LockContent> entry : entries) {
                String lockKey = entry.getKey();
                LockContent lockContent = entry.getValue();
                long expireTime = lockContent.getExpireTime();
                // 减少线程池中任务数量
                if ((expireTime - System.currentTimeMillis())/ 1000 < TIME_SECONDS_FIVE) {
                    //线程池异步续约
                    ThreadPool.submit(() -> {
                        boolean renew = redisDistributionLock.renew(lockKey, lockContent);
                        if (renew) {
                            long expireTimeNew = lockContent.getStartTime() + (expireTime - lockContent.getStartTime()) * 2 - TIME_SECONDS_FIVE * 1000;
                            lockContent.setExpireTime(expireTimeNew);
                        } else {
                            // 续约失败,说明已经执行完 OR redis 出现问题
                            lockContentMap.remove(lockKey);
                        }
                    });
                }
            }
        }
    }
}

Cinco, redis poço de replicação mestre-escravo

redisA solução mais comum para alta disponibilidade é 主从复制(mestre-escravo), que também redis分布式锁abre um buraco.

redis clusterEm um ambiente em cluster, se você A客户端deseja bloquear agora , ele selecionará um masternó para gravar de acordo com as regras de roteamento.Depois que key mylocko bloqueio for bem-sucedido, o masternó será keycopiado de forma assíncrona no slavecorrespondente .

Se o redis masternó estiver inativo no momento , para garantir a disponibilidade do cluster, ele continuará 主备切换e slavese tornará redis master. B客户端O masterbloqueio A客户端foi adicionado com sucesso no novo nó e eu pensei que tinha adicionado com sucesso.

Nesse momento, ele fará com que vários clientes concluam o bloqueio em um bloqueio distribuído ao mesmo tempo, resultando na geração de vários dados sujos.

Quanto à solução, atualmente, não há cura, apenas pode garantir a estabilidade da máquina e reduzir a probabilidade desse evento.

Sumário

A seguir estão Redisalguns dos poços que encontrei ao usar bloqueios distribuídos. Fiquei um pouco lamentado. Muitas vezes preenchi esse poço com um método. Não demorou muito para encontrar outro poço. O que é uma bala de prata, depois de ponderar os prós e os contras, escolhe um compromisso dentro do intervalo aceito.


Pequenos benefícios:

Existem alguns cursos pagos, gratuitos , para amigos. Preste atenção na minha conta pública [algo dentro do programador], responda [ 666 ]

Acho que você gosta

Origin blog.51cto.com/14787961/2489251
Recomendado
Clasificación