Implementação Java de exemplo de código de bloqueio distribuído baseado em redis

Por que existe essa demanda:

Por exemplo, uma operação simples do usuário, um thread para modificar o estado do usuário, primeiro leia o estado do usuário na memória, depois modifique-o na memória e armazene-o no banco de dados. Em um único tópico, isso não é problema. No entanto, no multithreading, como ler, modificar e gravar são três operações, não operações atômicas (sucesso ou falha ao mesmo tempo), haverá problemas de segurança de dados no multithreading.

Nesse caso, você pode usar bloqueios distribuídos para limitar a execução simultânea do programa.

Ideias de realização:

Isso significa que quando outros fios entrarem e funcionarem, eles desistirão ou tentarão novamente mais tarde se descobrirem que alguém está ocupando um assento.

Realização do placeholder:

Ele é implementado pelo comando setnx no redis. Para o comando redis, consulte meu blog https://www.cnblogs.com/javazl/p/12657280.html. O comando de definição padrão é salvar o valor. Quando o a chave existe, a definição é Substituirá o valor da chave, mas não a definição do conjunto. Quando não há chave, setnx entrará e tomará o lugar primeiro. Quando a chave existir, outro setnx não poderá entrar. . Espere até que a primeira execução seja concluída, libere o assento no comando del.

Código:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
     //setnx的返回值为long类型
      if (setnx == 1) {
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        //释放资源
         jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

No código acima, é uma implementação simples de bloqueio distribuído, mas há um problema. Ou seja, se ele desligar antes de liberar após ocupar. Então esse thread nunca será liberado, ou seja, o comando del não é chamado, todas as solicitações subsequentes são bloqueadas aqui e o bloqueio se torna um deadlock. Portanto, aqui precisa ser otimizado.

O método de otimização é adicionar um tempo de expiração para garantir que o bloqueio possa ser liberado após um determinado período de tempo.

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
      Long setnx = jedis.setnx("k1", "v1");
      if (setnx == 1) {
        //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl");
        String name = jedis.get("name");
        System.out.println(name);
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }

Após este tratamento, você pode garantir que o bloqueio pode ser desbloqueado normalmente. Mas haverá um novo problema, ou seja, se o servidor desligar durante a recuperação do bloqueio e definir o tempo de expiração, porque a recuperação do bloqueio, ou seja, setnx e definir o tempo de expiração são duas operações, que não são atômicas e não podem ser concluídas no mesmo tempo. Esse bloqueio ficará ocupado para sempre, não pode ser liberado e se torna um deadlock. Então, como resolver isso?

Após redis2.8, setnx e expireke podem ser executados juntos com um comando, e as duas operações tornam-se uma, o que resolverá este problema.

Realização otimizada:

public class LockTest {
  public static void main(String[] args) {
    Redis redis = new Redis();
    redis.execute(jedis->{
     //将两个操作合并成一个,nx就是setnx,ex就是expire
      String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
     //操作结果为okhuo或者error
      if (set !=null && "OK".equals(set)) {
     //给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
        jedis.expire("k1", 5);
        //没人占位
        jedis.set("name", "zl);
        String name = jedis.get("name");
        System.out.println(name);
      //释放资源
        jedis.del("k1");
      }else{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

Após otimizar com o tempo de expiração, embora o problema de deadlock seja resolvido, surge um novo problema, ou seja, o problema de timeout:

Por exemplo: se o negócio a ser executado é demorado, pode ser desordenado. Quando um thread local adquire o bloqueio, ele começa a executar o código de negócio, mas o código de negócio é demorado. Se o tempo de expiração for 3 segundos, e a execução do negócio requer 5 segundos, desta forma, o bloqueio será liberado mais cedo, e então o segundo thread adquire o bloqueio e inicia a execução. Quando a execução atinge o segundo segundo, o primeiro bloqueio também é executado. Neste momento, o primeiro thread irá liberar o bloqueio do segundo thread e, em seguida, o terceiro thread continuará a adquirir o bloqueio e executá-lo. Quando o terceiro segundo é alcançado Após a execução do segundo encadeamento, o bloqueio será liberado antecipadamente e o loop continuará a causar o caos do encadeamento.

Então, existem duas soluções principais

Tente evitar operações demoradas.
Para lidar com o bloqueio, defina um número aleatório ou string aleatória para o valor do bloqueio e julgue o valor do valor sempre que ele for liberado. Se for, libere, se não for, ele não o liberará. Por exemplo, suponha que o primeiro Quando o thread chegar, o valor que ele adquire o bloqueio é 1. Se ocorrer um tempo limite, ele entrará no próximo thread e o próximo thread adquirirá o novo valor como

3. Antes de liberar o segundo lugar, pegue o valor e compare-o.Se 1 não for igual a três, então a fechadura não é liberada.
Não há nada a dizer sobre o primeiro tipo, mas haverá um problema com o segundo tipo, ou seja, liberar o bloqueio irá verificar o valor, comparar e, em seguida, liberar. Haverá três operações, portanto não há atomicidade .Se esta operação for realizada, ele aparecerá. Deadlock. Aqui podemos usar scripts Lua para processar.

Recursos do Lua script:

1. Fácil de usar, o redis possui suporte integrado para scripts Lua.

2.Lua pode executar vários comandos redis atomicamente no servidor redis

3. Por motivos de rede, o desempenho do redis será afetado, portanto, o uso de Lua pode permitir a execução de vários comandos ao mesmo tempo, reduzindo os problemas de desempenho causados ​​pelo redis na rede.

Como usar scripts Lua no redis:

1. Escreva no servidor redis e, em seguida, chame o script no negócio java

2. Você pode escrever diretamente em java.Depois de escrever, quando precisar executá-lo, envie o script para redis para execução todas as vezes.

Crie um script Lua:

//用redis.call调用一个redis命令,调的是get命令,这个key是从外面传进来的keyif redis.call("get",KEYS[1])==ARGV[1] then//如果相等就去操作释放命令
  return redis.call("del",KEYS[1])
else
 return 0
end

Você pode encontrar uma soma SHA1 para o script Lua:

cat lua / equal.lua | redis-cli -a script de root load --pipe
script load Este comando armazenará em cache o script Lua no Redis e retornará o checksum SHA1 do conteúdo do script, e então passará SHA1 quando chamado em java O checksum é usado como parâmetro, para que o servidor redis saiba qual script executar.

Próxima escrita em java

public static void main(String[] args) {
    Redis redis = new Redis();
    for (int i = 0; i < 2; i++) {
      redis.execute(jedis -> {
        //1.先获取一个随机字符串
        String value = UUID.randomUUID().toString();
        //2.获取锁
        String k1 = jedis.set("k1", value, new SetParams().nx().ex(5));
        //3.判断是否成功拿到锁
        if (k1 != null && "OK".equals(k1)) {
          //4. 具体的业务操作
          jedis.set("site", "zl");
          String site = jedis.get("site");
          System.out.println(site);
          //5.释放锁
          jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", 
Arrays.asList("k1"), Arrays.asList(value));
        } else {
          System.out.println("没拿到锁");
        }
      });
    }
  }
}

Desta forma, o problema de deadlock é resolvido.

Algumas perguntas da entrevista de alta frequência coletadas no último 2020 (todas organizadas em documentos), há muitos produtos secos, incluindo mysql, netty, spring, thread, spring cloud, jvm, código-fonte, algoritmo e outras explicações detalhadas, bem como planos detalhados de aprendizagem, entrevistas, classificação de perguntas, etc. Para aqueles que precisam obter esses conteúdos, adicione Q como: 11604713672

Acho que você gosta

Origin blog.csdn.net/weixin_51495453/article/details/113936698
Recomendado
Clasificación