Maison  >  Article  >  Java  >  Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

Java后端技术全栈
Java后端技术全栈avant
2023-08-23 14:54:26665parcourir

Le contenu principal de cet article est le suivant :

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

1. Le problème des verrous locaux

Tout d'abord, passons en revue le problème des verrous locaux :

Actuellement, le microservice dans La question est divisée en quatre microservices Serve. Lorsque les demandes frontales arrivent, elles seront transmises à différents microservices. Si le front-end reçoit des requêtes de 10 W et que chaque microservice reçoit des requêtes de 2,5 W, si le cache échoue, chaque microservice se verrouille lors de l'accès à la base de données, via le verrou (synchronisé ou lock) pour verrouiller ses propres ressources de thread pour empêcher panne du cache. synchronziedlock)来锁住自己的线程资源,从而防止缓存击穿

这是一种本地加锁的方式,在分布式Il s'agit d'un Verrouillage local, dans Distribué entraînera des problèmes d'incohérence des données : par exemple, une fois que le service A a obtenu les données, il met à jour la clé de cache = 100 et le service B ne l'est pas. restreint par le verrou du service A et met simultanément à jour la clé de cache = 99. Le résultat final peut être 99 ou 100, mais il s'agit d'un état inconnu et incompatible avec le résultat attendu. L'organigramme est le suivant :

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

2. Qu'est-ce qu'un verrou distribué

Sur la base du problème de verrouillage local ci-dessus, nous avons besoin d'un verrou qui prend en charge environnement de cluster distribué : lors de l'interrogation de la base de données, un seul thread peut y accéder, et les autres threads peut y accéder. Vous devez attendre que le premier thread libère la ressource de verrouillage avant de poursuivre l'exécution.

Cas de la vie : Vous pouvez considérer la serrure comme une serrure à l'extérieur de la porte,所有并发线程比作 Ils veulent tous entrer dans la pièce, et une seule personne peut entrer dans la pièce. Lorsque quelqu'un entre, verrouillez la porte et les autres doivent attendre que la personne qui est entrée sorte.

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

Jetons un coup d'œil aux principes de base des verrous distribués, comme le montre la figure ci-dessous :

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

Analysons les verrous distribués dans l'image ci-dessus :

  • 1. Le front-end transmet les requêtes à haute concurrence de 10 W à quatre microservices thématiques.
  • 2. Chaque microservice gère 2,5 W de requêtes.
  • 3. Chaque thread traitant une requête doit saisir le verrou avant d'exécuter l'affaire. Cela peut être compris comme « occuper une fosse ».
  • 4. Le fil qui a acquis le verrou libère le verrou une fois l'affaire terminée. Cela peut être compris comme « libérer la fosse ».
  • 5. Le fil qui n'a pas été acquis doit attendre que le verrou soit libéré.
  • 6. Une fois le verrou libéré, d'autres fils saisissent le verrou.
  • 7. Répétez les étapes 4, 5 et 6.

Explication vernaculaire : Tous les fils de discussion demandés vont au même endroit"Occupez la fosse" , s'il y a un pit, la logique métier sera exécutée. S'il n'y a pas de pit, d'autres threads devront libérer le "pit". Cette fosse est visible par tous les threads. Vous pouvez placer cette fosse dans le cache ou la base de données Redis. Cet article explique comment utiliser Redis pour faire "Fosse distribuée". “占坑”,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”

三、Redis 的 SETNX

Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。

用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。

我们来看下这个命令,SETNXset If not exist的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。

在 Redis 命令行中是这样执行的:

set <key> <value> NX

我们可以进到 redis 容器中来试下 SETNX

3. SETNX de Redis

Redis, en tant que lieu accessible au public, peut être utilisé comme un lieu pour "en profiter".

Plusieurs solutions pour implémenter des verrous distribués à l'aide de Redis, nous utilisons tous la commande SETNX (clé de réglage égale à une certaine valeur). Seul le nombre de paramètres transmis dans le schéma haut de gamme est différent et les situations anormales sont prises en compte. 🎜🎜Jetons un coup d'œil à cette commande, SETNX est Abréviation de set If not exist. Cela signifie que lorsque la clé n'existe pas, définissez la valeur de la clé et lorsqu'elle existe, ne faites rien. 🎜🎜Voici comment il est exécuté dans la ligne de commande Redis : 🎜
docker exec -it <容器 id> redis-cli
🎜Nous pouvons aller dans le conteneur Redis et l'essayer SETNX. 🎜🎜Entrez d'abord le conteneur : 🎜
docker exec -it <容器 id> redis-cli

然后执行 SETNX 命令:将 wukong 这个 key 对应的 value 设置成 1111

set wukong 1111 NX

返回 OK,表示设置成功。重复执行该命令,返回 nil表示设置失败。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

四、青铜方案

我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。

3.1 青铜原理

我们来看下流程图:

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant
  • 多个并发线程都去 Redis 中申请锁,也就是执行 setnx 命令,假设线程 A 执行成功,说明当前线程 A 获得了。
  • 其他线程执行 setnx 命令都会是失败的,所以需要等待线程 A 释放锁。
  • 线程 A 执行完自己的业务后,删除锁。
  • 其他线程继续抢占锁,也就是执行 setnx 命令。因为线程 A 已经删除了锁,所以又有其他线程可以抢占到锁了。

代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent

setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
  // 2.抢占成功,执行业务
  List<TypeEntity> typeEntityListFromDb = getDataFromDB();
  // 3.解锁
  redisTemplate.delete("lock");
  return typeEntityListFromDb;
} else {
  // 4.休眠一段时间
  sleep(100);
  // 5.抢占失败,等待锁释放
  return getTypeEntityListByRedisDistributedLock();
}

一个小问题:那为什么需要休眠一段时间?

因为该程序存在递归调用,可能会导致栈空间溢出。

3.2 Inconvénients de la solution Bronze

Le bronze est appelé bronze car il est le plus élémentaire et va certainement causer beaucoup de problèmes.

Imaginez une scène de famille : La nuit, Xiao Kong déverrouille la porte seul et entre dans la pièce, allume la lumière ?, et puis tout à coup Le courant est coupé. Xiao Kong veut ouvrir la porte et sortir, mais ne trouve pas la position de verrouillage de la porte. Ensuite, Xiao Ming ne peut pas entrer, et ni l'un ni l'autre. n'importe qui dehors. 断电了,小空想开门出去,但是找不到门锁位置,那小明就进不去了,外面的人也进不来。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

从技术的角度看:setnx 占锁成功,业务代码出现异常或者服务器宕机,没有执行删除锁的逻辑,就造成了死锁

那如何规避这个风险呢?

设置锁的自动过期时间,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。

四、白银方案

4.1 生活中的例子

上面提到的青铜方案会有死锁问题,那我们就用上面的规避风险的方案来设计下,也就是我们的白银方案。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

还是生活中的例子:小空开锁成功后,给这款智能锁设置了一个沙漏倒计时⏳

D'un point de vue technique : setnx a occupé le verrou avec succès, lorsque le code métier est anormal ou le serveur est en panne, la logique de suppression du verrou n'est pas exécutée, ce qui entraîne. <h3 data-tool="mdnice编辑器" style="font-weight: bold;font-size: 20px;line-height: 1.4;padding-top: 10px;margin-top: 10px;margin-bottom: 5px;"> <span style="color: rgb(81, 81, 81);font-size: 1em;padding-left: 20px;border-left: 3px solid rgb(249, 191, 69);"></span>Alors comment éviter ce risque ? </h3> <p data-tool="mdnice编辑器" style="margin-bottom: 20px;line-height: 1.8em;color: rgb(58, 58, 58);"></p>Définir le verrouillage<code style="font-size: 14px;border-radius: 4px;font-family: " operator mono consolas monaco menlo monospace break-all rgb d>Délai d'expiration automatique, après un certain temps, le verrou est automatiquement supprimé , afin que d'autres threads puissent acquérir le verrou. 🎜

4. Plan Argent 🎜

🎜🎜4.1 Exemples dans la vie🎜🎜🎜Le plan Bronze mentionné ci-dessus aura des problèmes d'impasse, nous utiliserons donc le plan d'évitement des risques ci-dessus pour le concevoir, qui est notre plan argent. 🎜
🎜Un exemple tiré de la vie : après que Xiao Kong ait réussi à déverrouiller la porte , définissez un Compte à rebours du sablier⏳, une fois le sablier terminé, la porte se verrouille s'ouvre automatiquement. Même en cas de panne de courant soudaine dans la pièce, la serrure s'ouvrira automatiquement après un certain temps et d'autres pourront entrer. 🎜🎜🎜4.2 Schéma technique 🎜🎜🎜La différence avec la solution bronze est qu'une fois la serrure occupée avec succès, le délai d'expiration de la serrure est défini étape par étape. Comme le montre l'image ci-dessous : 🎜
Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

4.3 示例代码

清理 redis key 的代码如下

// 在 10s 以后,自动清理 lock
redisTemplate.expire("lock", 10, TimeUnit.SECONDS);

完整代码如下:

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
    // 2.在 10s 以后,自动清理 lock
    redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.解锁
    redisTemplate.delete("lock");
    return typeEntityListFromDb;
}

4.4 白银方案的缺陷

白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:

因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。

所以和青铜方案有一样的问题:锁永远不能过期

五、黄金方案

5.1 原子指令

上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。

原子性:多条命令要么都成功执行,要么都不执行。

将两步放在一步中执行:占锁+设置锁过期时间。

Redis 正好支持这种操作:

# 设置某个 key 的值并设置多少毫秒或秒 过期。
set <key> <value> PX <多少毫秒> NX
或
set <key> <value> EX <多少秒> NX

然后可以通过如下命令查看 key 的变化

ttl <key>

下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。

# 设置 key=wukong,value=1111,过期时间=5000ms
set wukong 1111 PX 5000 NX
# 查看 key 的状态
ttl wukong

执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

5.2 技术原理图

黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

5.3 示例代码

设置 lock 的值等于 123,过期时间为 10 秒。如果 10 秒 以后,lock 还存在,则清理 lock。

setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);

5.4 黄金方案的缺陷

我们还是举生活中的例子来看下黄金方案的缺陷。

5.4.1 L'utilisateur A préempte le verrou

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant
  • L'utilisateur A a d'abord préempté le verrou et configure le verrou pour qu'il se déverrouille automatiquement après 10 secondes. Le numéro de verrouillage est 123. 123
  • 10 秒以后,A 还在执行任务,此时锁被自动打开了。

5.4.2 用户 B 抢占锁

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant
  • 用户 B 看到房间的锁打开了,于是抢占到了锁,设置锁的编号为 123,并设置了过期时间 10 秒
  • 因房间内只允许一个用户执行任务,所以用户 A 和 用户 B 执行任务产生了冲突
  • 用户 A 在 15 s 后,完成了任务,此时 用户 B 还在执行任务。
  • 用户 A 主动打开了编号为 123
  • 10 secondes plus tard, A est toujours en train d'effectuer la tâche et la serrure s'ouvre automatiquement.
  • 5.4.2 L'utilisateur B saisit le verrou
    Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant
L'utilisateur B a vu que la serrure de la pièce était ouverte, il a donc saisi la serrure et a défini le numéro de serrure sur , et définissez le délai d'expiration<code style="font-size: 14px;border-radius: 4px;font-family: " operator mono consolas monaco menlo monospace break rgb>10 secondes.

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamantÉtant donné qu'un seul utilisateur est autorisé à effectuer des tâches dans la salle, l'utilisateur A et l'utilisateur B effectuent des tâches.
  • Utilisateur A dans 15 s plus tard, la tâche est terminée et l'utilisateur B est toujours en mission. 🎜🎜🎜🎜L'utilisateur A a pris l'initiative d'ouvrir le code numéroté 123 lock. 🎜🎜🎜🎜L'utilisateur B est toujours en train d'effectuer la tâche et constate que le verrou a été ouvert. 🎜🎜🎜🎜L'utilisateur B était très en colère : 🎜Je n'ai pas encore terminé la tâche, pourquoi la serrure s'est-elle ouverte ? 🎜🎜🎜🎜🎜5.4.3 L'utilisateur C saisit la serrure🎜🎜🎜🎜🎜🎜🎜Après que la serrure de l'utilisateur B soit activement ouverte par A, A quitte la pièce pendant que B est toujours en train d'effectuer des tâches.
  • L'utilisateur C saisit le verrou et C commence à exécuter la tâche.
  • Étant donné qu'un seul utilisateur est autorisé à effectuer des tâches dans la salle, il existe un conflit entre les tâches effectuées par l'utilisateur B et l'utilisateur C.

À partir du cas ci-dessus, nous pouvons savoir que parce que le temps nécessaire à l'utilisateur A pour traiter la tâche est supérieur au le temps nécessaire au nettoyage (déverrouillage) automatique de la serrure, donc une fois la serrure automatiquement déverrouillée, d'autres utilisateurs ont préempté le verrou. Lorsque l'utilisateur A aura terminé la tâche, il ouvrira activement les serrures saisies par les autres utilisateurs.

Pourquoi les serrures des autres sont-elles ouvertes ici ? Parce que les numéros de verrouillage sont tous appelés “123”,用户 A 只认锁编号,看见编号为 “123”, le verrou est ouvert. En conséquence, le verrou de l'utilisateur B est ouvert. À ce moment, l'utilisateur B n'a pas terminé la tâche, donc bien sûr il est en colère.

6. Plan Platine

6.1 Exemples dans la vie réelle

Les défauts du plan Or ci-dessus peuvent également être facilement résolus. Il serait préférable de définir des numéros différents pour chaque serrure~

comme indiqué dans. l'image ci-dessous montre que le verrou que B a préempté est bleu, ce qui est différent du verrou vert que A a préempté. De cette façon, il ne sera pas ouvert par A.

Réalisé une animation pour faciliter la compréhension :

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamantDémonstration d'animation
L'image statique est plus haute définition, vous pouvez jeter un oeil :

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

6.2 La différence entre le schéma technique

et la solution en or :

  • 设置锁的过期时间时,还需要设置唯一编号。
  • 主动删除锁的时候,需要判断锁的编号是否和设置的一致,如果一致,则认为是自己设置的锁,可以进行主动删除。
Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

6.3 代码示例

// 1.生成唯一 id
String uuid = UUID.randomUUID().toString();
// 2. 抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(lock) {
    System.out.println("抢占成功:" + uuid);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.获取当前锁的值
    String lockValue = redisTemplate.opsForValue().get("lock");
    // 5.如果锁的值和设置的值相等,则清理自己的锁
    if(uuid.equals(lockValue)) {
        System.out.println("清理锁:" + lockValue);
        redisTemplate.delete("lock");
    }
    return typeEntityListFromDb;
} else {
    System.out.println("抢占失败,等待锁释放");
    // 4.休眠一段时间
    sleep(100);
    // 5.抢占失败,等待锁释放
    return getTypeEntityListByRedisDistributedLock();
}
  • 1.生成随机唯一 id,给锁加上唯一值。
  • 2.抢占锁,并设置过期时间为 10 s,且锁具有随机唯一 id。
  • 3.抢占成功,执行业务。
  • 4.执行完业务后,获取当前锁的值。
  • 5.如果锁的值和设置的值相等,则清理自己的锁。

6.4 铂金方案的缺陷

上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant
  • 时刻:0s。线程 A 抢占到了锁。

  • 时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。

  • 时刻:10s。锁自动过期。

  • 时刻:11s。线程 B 抢占到锁。

  • 时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。

  • 时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。

那如何规避这个风险呢?钻石方案登场。

七、钻石方案

上面的线程 A 查询锁和删除锁的逻辑不是原子性的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。

7.1 技术原理图

如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。

Serrure distribuée Redis|Cinq plans d'évolution du bronze au diamant

7.2 代码示例

那如何用脚本进行删除呢?

我们先来看一下这段 Redis 专属脚本:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。

那么这段脚本怎么在 Java 项目中执行呢?

分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。

// 脚本解锁
String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

上面的代码中,KEYS[1] 对应“lock”,ARGV[1] 对应 “uuid”,含义就是如果 lock 的 value 等于 uuid 则删除 lock。

而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。

那钻石方案是不是就完美了呢?有没有更好的方案呢?

下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。

8. Résumé

Cet article étend la problématique des verrous distribués à travers la problématique des verrous locaux. Il présente ensuite cinq solutions de verrouillage distribué et explique les améliorations apportées aux différentes solutions, de peu profondes à profondes.

Grâce à l'évolution continue des solutions ci-dessus, nous savons où des situations anormales peuvent exister dans le système et comment mieux les gérer.

Par analogie, ce modèle de pensée évolutif peut également s'appliquer à d'autres technologies.

Ce qui suit résume les lacunes et les améliorations des cinq solutions ci-dessus.

Solution Bronze :

  • Défauts : le code commercial est anormal ou le serveur est en panne, et la logique de suppression active du verrou n'est pas exécutée, ce qui entraîne un blocage.
  • Amélioration : définissez le délai d'expiration automatique du verrou après un certain temps, le verrou sera automatiquement supprimé afin que d'autres threads puissent l'obtenir.

Silver Solution :

  • Défaut : L'occupation de la serrure et le réglage du délai d'expiration de la serrure s'effectuent en deux étapes et ne sont pas des opérations atomiques.
  • Amélioration : l'occupation des serrures et le réglage du délai d'expiration des serrures garantissent les opérations atomiques.

Golden Plan :

  • Défaut : Lorsque le verrou est activement supprimé, parce que les valeurs de verrouillage sont les mêmes, les verrous occupés par d'autres clients sont supprimés.
  • Amélioration : chaque fois que le verrou est occupé, il est défini de manière aléatoire sur une valeur plus grande. Lorsque le verrou est activement supprimé, la valeur du verrou est comparée à la valeur définie par vous-même pour voir si elle est égale.

Solution Platine :

  • Défauts : Ces trois étapes d'acquisition du verrou, de comparaison de la valeur du verrou et de suppression du verrou ne sont pas atomiques. Il est possible que le verrou ait expiré automatiquement à mi-chemin et ait été saisi par d'autres clients, entraînant la suppression des verrous occupés par d'autres clients lors de la suppression du verrou.
  • Amélioration : utilisez des scripts Lua pour effectuer des opérations atomiques d'acquisition de verrous, de comparaison de verrous et de suppression de verrous.

Plan Diamant:

  • Défauts : solution de verrouillage distribuée non professionnelle.
  • Amélioration : Verrouillage distribué par redission.

Le Plan Roi, rendez-vous dans le prochain article~

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer