Redis de bloqueo distribuido
Redisson
Introducir redission:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.4</version>
</dependency>
La clase de herramienta de bloqueo RLock se ha encapsulado en RedissonClient, y la uso directamente aquí:
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(); // 释放锁
}
}
}
Los resultados son los siguientes:
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
Realización de jedis
Implementemos manualmente bloqueos distribuidos de redis a través de jedis, y tengamos una comprensión más profunda del principio de implementación de bloqueos distribuidos de redis.
Presenta jedis:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
El SET key value NX PX miliseconds
:.comando de
¿Qué es un candado exitoso? Quien llame al comando set con la opción nx, la clave no existe, la configuración se realiza correctamente, de lo contrario falla, y quien establezca la clave correctamente obtendrá el bloqueo.
¿Qué debo hacer si el cliente cuelga? Puede establecer un período de tiempo de espera para la llave. Si el cliente cuelga después de que se bloqueó, la llave se eliminará al final del tiempo sin causar un interbloqueo.
¿Qué sucede si la clave está a punto de caducar y la empresa no se ha procesado dentro del período de tiempo de espera? Iniciar un hilo como clave para retrasar.
La implementación específica es la siguiente:
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));
}
}
El uso es el siguiente:
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); // 释放锁
}
}
}
Los resultados son los siguientes:
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
¿Por qué no dos pasos, primero de set key value
nuevo expire key millseconds
? Debido a que estas dos operaciones no son operaciones atómicas, si un cliente cuelga después de estar bloqueado, la clave nunca se eliminará, lo que provocará un interbloqueo.
¿Por qué iniciar un hilo de demonio para retrasar la clave? El subproceso del demonio se destruirá automáticamente cuando se cierre el subproceso que lo creó, sin cierre manual. La demora es para permitir que se complete la ejecución de la lógica empresarial y para evitar la caducidad de la clave y permitir que otros subprocesos tomen el bloqueo.
¿Por qué no enviar el del key
comando directamente al liberar el bloqueo ? Al liberar el bloqueo, es necesario verificar el valor para evitar que el bloqueo agregado por el proceso P1 sea liberado por otros procesos. Por lo tanto, la configuración del valor del valor también es exquisita. Solo el proceso P1 conoce este valor, por lo que solo él puede eliminar la llave cuando se libera.
para resumir
Ventajas: Los candados distribuidos basados en redis tendrán las características de redis, es decir, rápidos.
Desventajas: La lógica de implementación es compleja. Redis en sí es un modelo AP, que solo puede garantizar la partición y la disponibilidad de la red, pero no puede garantizar una coherencia sólida. La lógica de bloqueo distribuido es un modelo CP y la coherencia debe garantizarse, por lo que redis es un método de implementación Con cierta probabilidad, varios clientes adquirirán la cerradura. Por ejemplo, el nodo maestro en redis configura correctamente la clave y la devuelve al cliente. En este momento, se cuelga antes de que pueda sincronizarse con el esclavo, y luego el esclavo se elige como el nuevo nodo maestro. Otros clientes lograrán adquirir el bloqueo, de modo que varios clientes puedan adquirir el bloqueo al mismo tiempo.