Redis du verrou distribué
Redisson
Introduire la réduction:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.4</version>
</dependency>
La classe d'outils de verrouillage RLock a été encapsulée dans RedissonClient, et je l'utilise directement ici:
package com.morris.distribute.lock.redis.redisson;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 分布式锁之redis(redisson实现)
*
* @param id
*/
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
String key = "updateStatus" + id;
RLock lock = redissonClient.getLock(key);
lock.lock(); // 加锁
try {
Integer status = jdbcTemplate.queryForObject("select status from t_order where id=?", new Object[]{
id}, Integer.class);
if (Order.ORDER_STATUS_NOT_PAY == status) {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=? where id=? and status=?", new Object[]{
2, id, Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
}
log.info("updateStatus end, {}", id);
} finally {
lock.unlock(); // 释放锁
}
}
}
Les résultats sont les suivants:
2020-09-16 14:43:20,778 INFO [main] (Version.java:41) - Redisson 3.12.4
2020-09-16 14:43:21,298 INFO [redisson-netty-2-16] (ConnectionPool.java:167) - 1 connections initialized for 10.0.4.211/10.0.4.211:6379
2020-09-16 14:43:21,300 INFO [redisson-netty-2-19] (ConnectionPool.java:167) - 24 connections initialized for 10.0.4.211/10.0.4.211:6379
2020-09-16 14:43:21,371 INFO [t2] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:21,371 INFO [t1] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:21,371 INFO [t3] (OrderService.java:29) - updateStatus begin, 1
2020-09-16 14:43:24,610 INFO [t3] (OrderService.java:51) - updateStatus success, 1
2020-09-16 14:43:24,610 INFO [t3] (OrderService.java:58) - updateStatus end, 1
2020-09-16 14:43:24,620 INFO [t1] (OrderService.java:56) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:43:24,620 INFO [t1] (OrderService.java:58) - updateStatus end, 1
2020-09-16 14:43:24,630 INFO [t2] (OrderService.java:56) - updateStatus status already updated, ignore this request, 1
2020-09-16 14:43:24,630 INFO [t2] (OrderService.java:58) - updateStatus end, 1
Réalisation de jedis
Implémentons manuellement les verrous distribués redis via jedis et comprenons mieux le principe de l'implémentation des verrous distribués redis.
Présentez les jedis:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
Le raccourci clavier SET key value NX PX miliseconds
:.
Qu'est-ce qu'un verrou réussi? Quiconque appelle la commande set avec l'option nx, la clé n'existe pas, le paramètre réussit, sinon il échoue, et celui qui définit la clé avec succès obtiendra le verrou.
Que dois-je faire si le client raccroche? Vous pouvez définir un délai d'expiration pour la clé. Si le client raccroche après son verrouillage, la clé sera supprimée à la fin du temps sans provoquer de blocage.
Que faire si la clé est sur le point d'expirer et que l'entreprise n'a pas été traitée dans le délai d'expiration? Démarrez un thread comme clé pour retarder.
La mise en œuvre spécifique est la suivante:
package com.morris.distribute.lock.redis.my;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Slf4j
public class RedisLock {
@Autowired
private JedisPool jedisPool;
public void lock(String key, String value) {
for (; ;) {
// 自旋获取锁
if (tryLock(key, value)) {
return;
}
try {
TimeUnit.MILLISECONDS.sleep(100); // 这里暂时休眠100ms后再次获取锁,后续可以向AQS一样使用等待队列实现
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 尝试加锁
*
* @param key
* @param value
* @return
*/
private boolean tryLock(String key, String value) {
SetParams setParams = SetParams.setParams().nx().px(4_000); // 默认超时时间为4s
Jedis jedis = jedisPool.getResource();
String result = jedis.set(key, value, setParams);
if ("OK".equals(result)) {
Thread thread = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1); // 守护线程1s检测一下超时时间
} catch (InterruptedException e) {
e.printStackTrace();
}
Long ttl = jedis.pttl(key);
if (ttl < 2_000) {
// 当超时时间小于1/2时,增加超时时间到原来的4s
jedis.expire(key, 4_000);
log.info("add expire time for key : {}", key);
}
}
}, "expire1");
thread.setDaemon(true);
thread.start();
return true;
}
return false;
}
public void unlock(String key, String value) {
Jedis jedis = jedisPool.getResource();
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
}
}
L'utilisation est la suivante:
package com.morris.distribute.lock.redis.my;
import com.morris.distribute.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class OrderService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RedisLock redisLock;
/**
* 分布式锁之redis(jedis实现)
*
* @param id
*/
public void updateStatus(int id) {
log.info("updateStatus begin, {}", id);
String key = "updateStatus" + id;
String value = UUID.randomUUID().toString();
redisLock.lock(key, value);
try {
Integer status = jdbcTemplate.queryForObject("select status from t_order where id=?", new Object[]{
id}, Integer.class);
if (Order.ORDER_STATUS_NOT_PAY == status) {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
int update = jdbcTemplate.update("update t_order set status=? where id=? and status=?", new Object[]{
2, id, Order.ORDER_STATUS_NOT_PAY});
if (update > 0) {
log.info("updateStatus success, {}", id);
} else {
log.info("updateStatus failed, {}", id);
}
} else {
log.info("updateStatus status already updated, ignore this request, {}", id);
}
log.info("updateStatus end, {}", id);
} finally {
redisLock.unlock(key, value); // 释放锁
}
}
}
Les résultats sont les suivants:
2020-09-16 16:19:01,453 INFO [t2] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:01,453 INFO [t1] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:01,453 INFO [t3] (OrderService.java:28) - updateStatus begin, 1
2020-09-16 16:19:03,565 INFO [expire1] (RedisLock.java:53) - add expire time for key : updateStatus1
2020-09-16 16:19:04,748 INFO [t1] (OrderService.java:49) - updateStatus success, 1
2020-09-16 16:19:04,749 INFO [t1] (OrderService.java:56) - updateStatus end, 1
2020-09-16 16:19:04,801 INFO [t2] (OrderService.java:54) - updateStatus status already updated, ignore this request, 1
2020-09-16 16:19:04,801 INFO [t2] (OrderService.java:56) - updateStatus end, 1
2020-09-16 16:19:04,902 INFO [t3] (OrderService.java:54) - updateStatus status already updated, ignore this request, 1
2020-09-16 16:19:04,902 INFO [t3] (OrderService.java:56) - updateStatus end, 1
Pourquoi ne pas deux étapes, d'abord set key value
encore expire key millseconds
? Étant donné que ces deux opérations ne sont pas des opérations atomiques, si un client raccroche après avoir été verrouillé, la clé ne sera jamais supprimée, provoquant un blocage.
Pourquoi démarrer un thread démon pour retarder la clé? Le thread démon sera automatiquement détruit lorsque le thread qui l'a créé est fermé, sans arrêt manuel. Le délai consiste à permettre à l'exécution de la logique métier de se terminer, à éviter l'expiration de la clé et à laisser d'autres threads saisir le verrou.
Pourquoi ne pas envoyer la del key
commande directement lors du déverrouillage ? Lorsque vous libérez le verrou, vous devez vérifier la valeur pour éviter que le verrou ajouté par le processus P1 ne soit libéré par d'autres processus. Par conséquent, la définition de la valeur de valeur est également exquise. Seul le processus P1 connaît cette valeur, de sorte que lui seul peut supprimer la clé lorsqu'elle est libérée.
Pour résumer
Avantages: Les verrous distribués basés sur redis auront les caractéristiques de redis, c'est-à-dire rapides.
Inconvénients: la logique d'implémentation est complexe. Redis lui-même est un modèle AP, qui ne peut garantir que la partition et la disponibilité du réseau, mais ne peut pas garantir une forte cohérence. La logique de verrouillage distribué est un modèle CP et la cohérence doit être garantie, redis est donc une méthode d'implémentation Il est probable que plusieurs clients acquerront le verrou. Par exemple, le nœud maître dans redis réussit à définir la clé et à la renvoyer au client. À ce stade, il se bloque avant de pouvoir se synchroniser avec l'esclave, puis l'esclave est élu comme nouveau nœud maître. , D'autres clients réussiront à acquérir le verrou, de sorte que plusieurs clients puissent acquérir le verrou en même temps.