Exemple d'implémentation Java d'un code de verrouillage distribué basé sur Redis

Pourquoi y a-t-il une telle demande:

Par exemple, une simple opération utilisateur, un thread pour modifier l'état de l'utilisateur, lire d'abord l'état de l'utilisateur dans la mémoire, puis le modifier dans la mémoire, puis le stocker dans la base de données. En fil unique, ce n'est pas un problème. Cependant, dans le multithreading, comme la lecture, la modification et l'écriture sont trois opérations et non des opérations atomiques (succès ou échec en même temps), il y aura des problèmes de sécurité des données dans le multithreading.

Dans ce cas, vous pouvez utiliser des verrous distribués pour limiter l'exécution simultanée du programme.

Idées de réalisation:

Cela signifie que lorsque d'autres threads entrent et fonctionnent, ils abandonneront ou réessayeront plus tard s'ils trouvent que quelqu'un occupe un siège.

Réalisation d'espace réservé:

Il est implémenté par la commande setnx dans redis. Pour la commande redis, veuillez consulter mon blog https://www.cnblogs.com/javazl/p/12657280.html. La commande set par défaut consiste à enregistrer la valeur. Lorsque le key existe, set is Cela écrasera la valeur de la clé, mais pas setnx. Lorsqu'il n'y a pas de clé, setnx entrera et prendra la place en premier. Lorsque la clé existe, les autres setnx ne pourront pas entrer. . Attendez que la première exécution soit terminée, libérez le siège dans la commande del.

Code:

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{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

Dans le code ci-dessus, il s'agit d'une implémentation simple de verrou distribué, mais il y a un problème. Autrement dit, s'il raccroche avant de relâcher après avoir occupé. Ensuite, ce thread ne sera jamais libéré, c'est-à-dire que la commande del n'est pas appelée, toutes les demandes suivantes sont bloquées ici et le verrou devient un blocage. Donc ici doit être optimisé.

La méthode d'optimisation consiste à ajouter un délai d'expiration pour garantir que le verrou puisse être libéré après un certain laps de temps.

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{
        //有人占位,停止/暂缓 操作
      }
    });
  }

Après ce traitement, vous pouvez vous assurer que le verrou peut être libéré normalement. Mais il y aura un nouveau problème, c'est-à-dire si le serveur raccroche pendant la récupération des verrous et la définition de l'heure d'expiration, car la récupération des verrous, c'est-à-dire setnx et la définition de l'heure d'expiration sont deux opérations, qui ne sont pas atomiques et ne peuvent pas être effectuées à la en même temps. Ce verrou sera occupé pour toujours, ne peut pas être libéré et devient une impasse. Alors, comment le résoudre?

Après redis2.8, setnx et expireke peuvent être exécutés avec une seule commande, et les deux opérations deviennent une, ce qui résoudra ce problème.

Réalisation optimisée:

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{
        //有人占位,停止/暂缓 操作
      }
    });
  }
}

Après l'optimisation du délai d'expiration, bien que le problème de blocage soit résolu, un nouveau problème survient, c'est-à-dire le problème de délai d'expiration:

Par exemple: si l'entreprise à exécuter prend du temps, elle peut être désordonnée. Lorsqu'un thread local acquiert le verrou, il commence à exécuter le code métier, mais le code métier prend du temps. Si le délai d'expiration est de 3 secondes, et l'exécution de l'entreprise prend 5 secondes, de cette manière, le verrou sera libéré plus tôt, puis le deuxième thread acquiert le verrou et commence l'exécution. Lorsque l'exécution atteint la deuxième seconde, le premier verrou est également exécuté. À ce stade, le premier thread libère le verrou du second thread, puis le troisième thread continue d'acquérir le verrou et de l'exécuter. Lorsque le troisième second est atteint Une fois le deuxième thread exécuté, le verrou sera libéré à l'avance et la boucle continuera à provoquer le chaos des threads.

Ensuite, il y a deux solutions principales

Essayez d'éviter les opérations chronophages.
Pour gérer le verrou, définissez un nombre aléatoire ou une chaîne aléatoire sur la valeur du verrou et évaluez la valeur de la valeur à chaque fois qu'il est libéré. ​​Si tel est le cas, relâchez-le, sinon, il ne le libèrera pas. Par exemple, supposons le premier Lorsque le thread arrive, la valeur qu'il acquiert le verrou est 1. Si un délai d'attente se produit, il entrera le thread suivant et le thread suivant acquerra la nouvelle valeur comme

3. Avant de libérer la deuxième place, obtenez la valeur et comparez-la. Si 1 n'est pas égal à trois, le verrou n'est pas libéré.
Il n'y a rien à dire sur le premier type, mais il y aura un problème avec le second type, c'est-à-dire que la libération du verrou vérifiera la valeur, puis comparera, puis relâchera. Il y aura trois opérations, donc il n'y a pas d'atomicité Si cette opération est effectuée, elle apparaîtra. Ici, nous pouvons utiliser des scripts Lua pour traiter.

Caractéristiques du script Lua:

1. Facile à utiliser, redis a un support intégré pour les scripts Lua.

2.Lua peut exécuter plusieurs commandes redis de manière atomique sur le serveur redis

3. Pour des raisons de réseau, les performances de redis seront affectées.Par conséquent, l'utilisation de Lua peut permettre d'exécuter plusieurs commandes en même temps, réduisant ainsi les problèmes de performances causés par le réseau à redis.

Comment utiliser les scripts Lua dans Redis:

1. Écrivez-le sur le serveur redis, puis appelez le script dans l'entreprise java

2. Vous pouvez écrire directement en java. Après l'avoir écrit, lorsque vous avez besoin de l'exécuter, envoyez le script à redis pour exécution à chaque fois.

Créez un 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

Vous pouvez trouver une somme SHA1 pour le script Lua:

cat lua / equal.lua | redis-cli -a root script load --pipe
script load Cette commande mettra en cache le script Lua dans Redis et retournera la somme de contrôle SHA1 du contenu du script, puis passera en SHA1 lorsqu'elle est appelée en java La somme de contrôle est utilisé comme paramètre, afin que le serveur redis sache quel script exécuter.

Ecrire ensuite en 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("没拿到锁");
        }
      });
    }
  }
}

De cette manière, le problème de blocage est résolu.

Certaines questions d'entretien à haute fréquence collectées dans le dernier 2020 (toutes organisées en documents), il existe de nombreux produits secs, y compris mysql, netty, spring, thread, spring cloud, jvm, code source, algorithme et autres explications détaillées, ainsi que plans d'apprentissage détaillés, entretiens Tri des questions, etc. Pour ceux qui ont besoin d'obtenir ces contenus, veuillez ajouter Q comme: 11604713672

Je suppose que tu aimes

Origine blog.csdn.net/weixin_51495453/article/details/113936698
conseillé
Classement