Quelles sont les trois implémentations de verrous distribués ?

Trois implémentations de verrous distribués :

  1. Réaliser un verrouillage distribué basé sur une base de données ;
  2. Implémenter des verrous distribués basés sur le cache (Redis, etc.) ;
  3. Implémenter des verrous distribués basés sur Zookeeper ;

1. Réaliser un verrouillage distribué basé sur la base de données

  1. serrure pessimiste

Utilisez select ... où ... pour mettre à jour le verrouillage exclusif

Remarque : Les autres fonctions supplémentaires sont fondamentalement les mêmes que celles de l'implémentation 1. Ce qu'il faut noter ici est "where name=lock", le champ de nom doit être indexé, sinon la table sera verrouillée. Dans certains cas, comme les petites tables, l'optimiseur MySQL n'utilisera pas cet index, ce qui entraînera des problèmes de verrouillage des tables.

  1. serrure optimiste

La plus grande différence entre le verrouillage dit optimiste et les précédents est qu'il est basé sur l'idée du CAS. Il ne s'exclut pas mutuellement et ne consomme pas de ressources en raison de l'attente du verrouillage. Pendant l'opération, il est considéré qu'il n'y a pas de conflit de concurrence, qui ne peut être détecté qu'après l'échec de la version de mise à jour. Nos achats de panique et nos ventes flash utilisent ce type de mise en œuvre pour éviter la survente.
Le verrouillage optimiste est obtenu en augmentant le champ du numéro de version incrémenté

2. Implémentez des verrous distribués basés sur le cache (Redis, etc.)

  1. Introduction de la commande :
    (1) SETNX
    SETNX key val : Si et seulement si la clé n'existe pas, définissez une chaîne dont la clé est val et renvoyez 1 ; si la clé existe, ne faites rien et renvoyez 0.
    (2) expiration
    du délai d'expiration de la clé : définissez un délai d'expiration pour la clé, l'unité est en deuxième, au-delà de ce délai, le verrou sera automatiquement libéré pour éviter un blocage.
    (3) supprimer
    la clé de suppression : supprimer la clé

Lors de l'utilisation de Redis pour implémenter des verrous distribués, ces trois commandes sont principalement utilisées.

  1. Idées d'implémentation :
    (1) Lors de l'acquisition d'un verrou, utilisez setnx pour ajouter un verrou et utilisez la commande expire pour ajouter un délai d'expiration pour le verrou, et le verrou sera libéré automatiquement après le délai d'attente, et la valeur du verrou est un UUID généré aléatoirement, qui est libéré via ce jugement, est effectué lors du verrouillage.
    (2) Lors de l'acquisition du verrou, un délai d'attente d'acquisition est également défini. Si ce délai est dépassé, l'acquisition du verrou sera abandonnée.
    (3) Lors de la libération du verrou, déterminez s'il s'agit du verrou par UUID. Si c'est le verrou, exécutez delete pour libérer le verrou.

  2. Code d'implémentation simple du verrouillage distribué :

/**
    * 分布式锁的简单实现代码    */
   public class DistributedLock {
    
    
   
       private final JedisPool jedisPool;
   
       public DistributedLock(JedisPool jedisPool) {
    
    
          this.jedisPool = jedisPool;
      }
  
      /**
       * 加锁
       * @param lockName       锁的key
       * @param acquireTimeout 获取超时时间
       * @param timeout        锁的超时时间
       * @return 锁标识
       */
      public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
    
    
          Jedis conn = null;
          String retIdentifier = null;
          try {
    
    
              // 获取连接
              conn = jedisPool.getResource();
              // 随机生成一个value
              String identifier = UUID.randomUUID().toString();
              // 锁名,即key值
              String lockKey = "lock:" + lockName;
              // 超时时间,上锁后超过此时间则自动释放锁
              int lockExpire = (int) (timeout / );
  
              // 获取锁的超时时间,超过这个时间则放弃获取锁
              long end = System.currentTimeMillis() + acquireTimeout;
              while (System.currentTimeMillis() < end) {
    
    
                  if (conn.setnx(lockKey, identifier) == ) {
    
    
                      conn.expire(lockKey, lockExpire);
                      // 返回value值,用于释放锁时间确认
                      retIdentifier = identifier;
                      return retIdentifier;
                  }
                  // 返回-代表key没有设置超时时间,为key设置一个超时时间
                  if (conn.ttl(lockKey) == -) {
    
    
                      conn.expire(lockKey, lockExpire);
                  }
  
                  try {
    
    
                      Thread.sleep();
                  } catch (InterruptedException e) {
    
    
                      Thread.currentThread().interrupt();
                  }
              }
          } catch (JedisException e) {
    
    
              e.printStackTrace();
          } finally {
    
    
              if (conn != null) {
    
    
                  conn.close();
              }
          }
          return retIdentifier;
      }
  
      /**
       * 释放锁
       * @param lockName   锁的key
       * @param identifier 释放锁的标识
       * @return
       */
      public boolean releaseLock(String lockName, String identifier) {
    
    
          Jedis conn = null;
          String lockKey = "lock:" + lockName;
          boolean retFlag = false;
          try {
    
    
              conn = jedisPool.getResource();
              while (true) {
    
    
                  // 监视lock,准备开始事务
                  conn.watch(lockKey);
                  // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁
                  if (identifier.equals(conn.get(lockKey))) {
    
    
                      Transaction transaction = conn.multi();
                      transaction.del(lockKey);
                     List<Object> results = transaction.exec();
                     if (results == null) {
    
    
                         continue;
                     }
                     retFlag = true;
                 }
                 conn.unwatch();
                 break;
             }
         } catch (JedisException e) {
    
    
             e.printStackTrace();
         } finally {
    
    
             if (conn != null) {
    
    
                 conn.close();
             }
         }
         return retFlag;
     }
 }
  1. Testez le verrou distribué qui vient d'être implémenté

Dans l'exemple, 50 threads sont utilisés pour simuler une vente flash d'un produit, et l'opérateur - est utilisé pour réduire le produit. L'ordre des résultats permet de voir s'il est dans un état verrouillé.

Simulez le service seckill, dans lequel le pool de threads jedis est configuré et transmis au verrou distribué lors de l'initialisation pour son utilisation.

public class Service {
    
    

    private static JedisPool pool = null;

    private DistributedLock lock = new DistributedLock(pool);

    int n = 500;

    static {
    
    
        JedisPoolConfig config = new JedisPoolConfig();
        // 设置最大连接数
        config.setMaxTotal(200);
        // 设置最大空闲数
        config.setMaxIdle(8);
        // 设置最大等待时间
        config.setMaxWaitMillis(1000 * 100);
        // 在borrow一个jedis实例时,是否需要验证,若为true,则所有jedis实例均是可用的
        config.setTestOnBorrow(true);
        pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
    }

    public void seckill() {
    
    
        // 返回锁的value值,供释放锁时候进行判断
        String identifier = lock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "获得了锁");
        System.out.println(--n);
        lock.releaseLock("resource", identifier);
    }
}

Simuler des threads pour le service Spike ;

public class ThreadA extends Thread {
    
    
    private Service service;

    public ThreadA(Service service) {
    
    
        this.service = service;
    }

    @Override
    public void run() {
    
    
        service.seckill();
    }
}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Service service = new Service();
        for (int i = 0; i < 50; i++) {
    
    
            ThreadA threadA = new ThreadA(service);
            threadA.start();
        }
    }
}

Le résultat est le suivant, le résultat est ordonné :
insérer la description de l'image ici
Si vous commentez la partie qui utilise le verrou :

public void seckill() {
    
    
    // 返回锁的value值,供释放锁时候进行判断
    //String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
    System.out.println(Thread.currentThread().getName() + "获得了锁");
    System.out.println(--n);
    //lock.releaseLock("resource", indentifier);
}

Les résultats montrent que certains sont asynchrones :
insérer la description de l'image ici
3. Implémenter des verrous distribués basés sur Zookeeper

ZooKeeper est un composant open source qui fournit des services cohérents pour les applications distribuées. À l'intérieur se trouve une arborescence de répertoires de système de fichiers hiérarchique, qui stipule qu'il ne peut y avoir qu'un seul nom de fichier unique dans le même répertoire. Les étapes pour implémenter des verrous distribués basés sur ZooKeeper sont les suivantes :

(1) Créer un répertoire mylock ;
(2) Le thread A veut acquérir un verrou et crée un nœud séquentiel temporaire dans le répertoire mylock ;
(3) Obtenir tous les nœuds enfants dans le répertoire mylock, puis obtenir des nœuds frères plus petits que lui, s'il n'existe pas, cela signifie que le numéro de séquence du thread actuel est le plus petit et que le verrou est obtenu ;
(4) Le thread B obtient tous les nœuds, juge qu'il ne s'agit pas du plus petit nœud et se met à surveiller le nœud qui est plus petit que lui-même ;
(5) Le thread A termine le traitement, supprime son propre nœud, le thread B écoute l'événement de changement, juge s'il s'agit du plus petit nœud et obtient le verrou si c'est le cas.

Ici, nous recommandons Curator, une bibliothèque open source Apache, qui est un client ZooKeeper. L'InterProcessMutex fourni par Curator est une implémentation de verrous distribués. La méthode acquire est utilisée pour acquérir des verrous et la méthode release est utilisée pour libérer les verrous.

Le code source d'implémentation est le suivant :

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * 分布式锁Zookeeper实现
 *
 */
@Slf4j
@Component
public class ZkLock implements DistributionLock {
    
    
private String zkAddress = "zk_adress";
    private static final String root = "package root";
    private CuratorFramework zkClient;

    private final String LOCK_PREFIX = "/lock_";

    @Bean
    public DistributionLock initZkLock() {
    
    
        if (StringUtils.isBlank(root)) {
    
    
            throw new RuntimeException("zookeeper 'root' can't be null");
        }
        zkClient = CuratorFrameworkFactory
                .builder()
                .connectString(zkAddress)
                .retryPolicy(new RetryNTimes(2000, 20000))
                .namespace(root)
                .build();
        zkClient.start();
        return this;
    }

    public boolean tryLock(String lockName) {
    
    
        lockName = LOCK_PREFIX+lockName;
        boolean locked = true;
        try {
    
    
            Stat stat = zkClient.checkExists().forPath(lockName);
            if (stat == null) {
    
    
                log.info("tryLock:{}", lockName);
                stat = zkClient.checkExists().forPath(lockName);
                if (stat == null) {
    
    
                    zkClient
                            .create()
                            .creatingParentsIfNeeded()
                            .withMode(CreateMode.EPHEMERAL)
                            .forPath(lockName, "1".getBytes());
                } else {
    
    
                    log.warn("double-check stat.version:{}", stat.getAversion());
                    locked = false;
                }
            } else {
    
    
                log.warn("check stat.version:{}", stat.getAversion());
                locked = false;
            }
        } catch (Exception e) {
    
    
            locked = false;
        }
        return locked;
    }

    public boolean tryLock(String key, long timeout) {
    
    
        return false;
    }

    public void release(String lockName) {
    
    
        lockName = LOCK_PREFIX+lockName;
        try {
    
    
            zkClient
                    .delete()
                    .guaranteed()
                    .deletingChildrenIfNeeded()
                    .forPath(lockName);
            log.info("release:{}", lockName);
        } catch (Exception e) {
    
    
            log.error("删除", e);
        }
    }

    public void setZkAddress(String zkAddress) {
    
    
        this.zkAddress = zkAddress;
    }
}

Avantages : il présente les caractéristiques de haute disponibilité, de réentrance et de verrouillage bloquant, ce qui peut résoudre le problème des blocages invalides.

Inconvénients : en raison de la nécessité de créer et de supprimer fréquemment des nœuds, les performances ne sont pas aussi bonnes que la méthode Redis.

Quatre, contraste

Inconvénients de la mise en œuvre du verrouillage distribué de la base de données
 :

1. Les performances des opérations de base de données sont médiocres et il existe un risque de verrouillage des tables.
2. Après l'échec des opérations non bloquantes, une interrogation est requise, ce qui consomme des ressources CPU.
3. Sans engagement à long terme ou à long terme l'interrogation peut occuper plus de ressources de connexion

Implémentation du verrouillage distribué Redis (cache)
Inconvénients :

1. Le délai d'expiration de l'échec de la suppression du verrou n'est pas facile à contrôler.
2. Non bloquant, après l'échec de l'opération, il doit interroger et occuper les ressources du processeur ;

Inconvénients de l'implémentation du verrouillage distribué ZK
: les performances ne sont pas aussi bonnes que l'implémentation Redis, la raison principale est que les opérations d'écriture (acquisition et libération des verrous) doivent être exécutées sur le leader, puis synchronisées avec le suiveur.

En bref : ZooKeeper offre de meilleures performances et une meilleure fiabilité.

Du point de vue de la facilité de compréhension (faible à élevée) base de données> cache> Zookeeper

Du point de vue de la complexité de mise en œuvre (de faible à élevée) Zookeeper >= cache > base de données

D'un point de vue performances (de haut en bas) cache > Zookeeper >= base de données

Du point de vue de la fiabilité (de haut en bas) Zookeeper > Cache > Base de données

Je suppose que tu aimes

Origine blog.csdn.net/weixin_45817985/article/details/132581093
conseillé
Classement