English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Generalmente, hay tres formas de implementar candado distribuido:1.Optimista candado de base de datos;2.Distribuido candado basado en Redis;3.Distribuido candado basado en ZooKeeper. Este blog presentará el segundo método, basado en Redis para implementar candado distribuido. Aunque en línea hay varios blogs que introducen la implementación de candado distribuido Redis, sin embargo, su implementación tiene varios problemas, para evitar engañar a los demás, este blog detallará cómo implementar correctamente el candado distribuido Redis.
Fiabilidad
Primero, para garantizar que el candado distribuido sea utilizable, al menos debemos asegurarnos de que la implementación del candado cumpla con las siguientes cuatro condiciones:
Mutualidad. En cualquier momento, solo un cliente puede mantener el candado.
No ocurrirá un bloqueo muerto. Incluso si un cliente se descompone mientras mantiene el candado sin desbloquearlo activamente, se garantiza que otros clientes posteriores puedan bloquear.
Tiene tolerancia a fallos. Si la mayoría de los nodos Redis funcionan correctamente, el cliente puede bloquear y desbloquear.
Para desatar, hay que atar primero. El bloqueo y el desbloqueo deben ser realizados por el mismo cliente, el cliente no puede desbloquear el candado de otro cliente.
Implementación del código
Dependencia del componente
Primero, debemos introducir el componente de código abierto Jedis a través de Maven, agregar el siguiente código al archivo pom.xml:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>
Código de bloqueo
Postura correcta
Las palabras son baratas, muéstrame el código. Primero muestra el código, luego explica por qué se realiza así:
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * Intentar obtener un candado distribuido * @param jedis Cliente Redis * @param lockKey Bloqueo * @param requestId Identificador de solicitud * @param expireTime Tiempo de expiración * @return Si se ha obtenido con éxito */ public static Boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
Se puede ver que el bloqueo se realiza en una sola línea de código: jedis.set(String key, String value, String nxxx, String expx, int time), este método set() tiene cinco parámetros formales:
El primero es key, utilizamos key como bloqueo porque la clave es única.
El segundo es value, enviamos requestId, muchos estudiantes pueden no entender, ¿no es suficiente tener una clave como bloqueo? ¿Por qué还需要使用value? La razón es que cuando hablamos de confiabilidad, el bloqueo distribuido debe satisfacer la cuarta condición: 'Para desbloquear, se debe saber quién lo bloqueó'. Al asignar el valor de value como requestId, sabemos qué solicitud bloqueó esta clave, y podemos tener una base para el desbloqueo. El requestId se puede generar utilizando el método UUID.randomUUID().toString().
El tercero es nxxx, este parámetro que llenamos es NX, lo que significa SETIFNOTEXIST, es decir, cuando la clave no existe, realizamos la operación set; si la clave ya existe, no realizamos ninguna operación;
El cuarto es expx, este parámetro que enviamos es PX, lo que significa que queremos agregar una configuración de expiración a esta clave, y el tiempo específico se determina por el quinto parámetro.
El quinto es time, que coincide con el cuarto parámetro, representando el tiempo de expiración de la clave.
En resumen, la ejecución del método set() anterior solo puede generar dos resultados:1.Si no existe un bloqueo actual (la clave no existe), se realiza la operación de bloqueo y se configura un tiempo de expiración para el bloqueo, y el valor representa el cliente que realiza el bloqueo.2.No se realiza ninguna operación si ya existe un bloqueo.
Los estudiantes atentos se darán cuenta de que nuestro código de bloqueo satisface las tres condiciones descritas en nuestra descripción de confiabilidad. Primero, el parámetro NX de set() garantiza que si ya existe una clave, la función no se ejecutará con éxito, lo que significa que solo un cliente puede poseer el bloqueo, satisfaciendo la exclusividad. En segundo lugar, dado que hemos configurado un tiempo de expiración para el bloqueo, incluso si el poseedor del bloqueo se colapsa sin desbloquear, el bloqueo se desbloqueará automáticamente (es decir, la clave se eliminará) cuando alcance el tiempo de expiración, evitando el bloqueo muerto. Finalmente, ya que solo consideramos el escenario de implementación de Redis en un solo servidor, por el momento no nos preocupamos por la tolerancia a fallos.
Ejemplo de error1
Un ejemplo de error común es usar la combinación de jedis.setnx() y jedis.expire() para implementar el acoplamiento, el código es el siguiente:
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { long result = jedis.setnx(lockKey, requestId); if (result == 1) { // Si aquí el programa se cae repentinamente, no se puede configurar el tiempo de expiración, ocurrirá un candado muerto jedis.expire(lockKey, expireTime); } }
El método setnx() actúa como SETIFNOTEXIST, el método expire() es para añadir un tiempo de expiración al candado. A primera vista parece que da el mismo resultado que el método set() anterior, sin embargo, debido a que son dos comandos Redis, no son atómicos; si el programa se cae repentinamente después de ejecutar setnx(), lo que lleva a que el candado no tenga una hora de expiración configurada. Entonces ocurrirá un candado muerto. La razón por la que algunas personas implementan de esta manera es porque las versiones bajas de jedis no soportan el método set() con múltiples parámetros.
Ejemplo de error2
Este ejemplo de error es más difícil de detectar y su implementación es más compleja. La idea de implementación: usar el comando jedis.setnx() para acoplar, donde la clave es el candado y el valor es el tiempo de expiración del candado. El proceso de ejecución:1.intenta acoplar usando el método setnx() si el candado actual no existe, devuelve que se ha logrado acoplar con éxito.2.si el candado ya existe, obtener el tiempo de expiración del candado y compararlo con el tiempo actual; si el candado ya ha expirado, establecer una nueva hora de expiración y devolver que se ha logrado acoplar con éxito. El código es el siguiente:
public static Boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { long expires = System.currentTimeMillis() + expireTime; String expiresStr = String.valueOf(expires); // Si el bloqueo actual no existe, devolverá el éxito al bloquear if (jedis.setnx(lockKey, expiresStr) == 1) { return true; } // Si el bloqueo existe, obtener la hora de expiración del bloqueo String currentValueStr = jedis.get(lockKey); if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) { // El bloqueo ha expirado, obtener la hora de expiración del bloqueo anterior y configurar la hora de expiración del bloqueo actual String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // Considerando la situación de concurrencia multihilo, solo un hilo tiene derecho a bloquearse si el valor de configuración y el valor actual son iguales return true; } } // En otras circunstancias, devolverá el fracaso al bloquear return false; }
Entonces, ¿dónde está el problema de este código?1.Dado que el cliente genera el tiempo de expiración por sí mismo, se debe exigir que los clientes distribuidos tengan la sincronización del tiempo.2.Cuando el bloqueo expira, si varios clientes ejecutan simultáneamente el método jedis.getSet(), aunque finalmente solo un cliente puede bloquearse, la hora de expiración del bloqueo de este cliente puede ser cubierta por otros clientes.3.El bloqueo no tiene un identificador de propietario, es decir, cualquier cliente puede desbloquearlo.
Código de desbloqueo
Postura correcta
Mejor que mostrar el código primero y luego explicar por qué se realiza de esta manera:
public class RedisTool { private static final long RELEASE_SUCCESS = 1L; /** * Liberación de bloqueo distribuido * @param jedis Cliente Redis * @param lockKey Bloqueo * @param requestId Identificador de solicitud * @return Si se libera con éxito */ public static Boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
Se puede ver que solo se necesitan dos líneas de código para desbloquear. La primera línea de código, escribimos un código Lua simple, la última vez que vimos este lenguaje de programación fue en 'Hackers & Painters', ¡pero esta vez lo estamos usando!. La segunda línea de código, enviamos el código Lua al método jedis.eval() y pasamos los parámetros KEYS[1] asignar el valor lockKey, ARGV[1] asignar el valor requestId. El método eval() es entregar el código Lua al servidor Redis.
Entonces, ¿cuál es la función de este código Lua? En realidad es muy simple, primero obtener el valor correspondiente al bloqueo, verificar si es igual al requestId, si es igual, eliminar el bloqueo (desbloquear). ¿Por qué se utiliza el lenguaje Lua para implementar? Porque se asegura de que las operaciones anteriores sean atómicas. Sobre los problemas que puede traer la no atomicidad, puede leer 【código de desbloqueo-Ejemplo de error2】. Entonces, ¿por qué ejecutar el método eval() puede garantizar la atomicidad? Esto se debe a las características de Redis, a continuación se muestra parte de la explicación del comando eval en el sitio web oficial:
En términos simples, es decir, cuando se ejecuta el comando eval, el código Lua se ejecuta como un comando y Redis solo ejecutará otros comandos después de que el comando eval se complete.
Ejemplo de error1
El código de desbloqueo más común es usar directamente el método jedis.del() para eliminar el bloqueo. Este método de desbloqueo que no verifica primero al propietario del bloqueo y desbloquea directamente puede llevar a que cualquier cliente pueda desbloquear en cualquier momento, incluso si este bloqueo no es suyo.
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); }
Ejemplo de error2
Este código de desbloqueo a primera vista también parece estar bien, incluso yo casi lo implementé de esta manera, casi igual que la postura correcta, la única diferencia es que se ejecuta en dos comandos separados, el código es el siguiente:
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // Determinar si el bloqueo y el desbloqueo son del mismo cliente if (requestId.equals(jedis.get(lockKey))) { // Si en este momento, el bloqueo no pertenece a este cliente, se puede entender mal el bloqueo jedis.del(lockKey); } }
Como se indica en los comentarios del código, el problema radica en que si se llama al método jedis.del() y la llave del bloqueo ya no pertenece al cliente actual, se liberará el bloqueo de otro cliente. ¿Existe realmente esta situación? La respuesta es sí, por ejemplo, si el cliente A bloquea, después de un tiempo el cliente A desbloquea, antes de ejecutar jedis.del(), el bloqueo se vence repentinamente, y en este momento el cliente B intenta bloquear con éxito, luego el cliente A ejecuta el método del(), liberando el bloqueo del cliente B.
Resumen
Este artículo introduce cómo implementar correctamente el bloqueo distribuido de Redis utilizando código Java, y también proporciona dos ejemplos clásicos de errores de bloqueo y desbloqueo. En realidad, no es difícil implementar un bloqueo distribuido utilizando Redis, siempre y cuando se puedan cumplir las cuatro condiciones de confiabilidad.
¿En qué escenarios se utiliza principalmente el bloqueo distribuido? En lugares que necesitan sincronización, por ejemplo, al insertar un registro de datos, es necesario verificar previamente si hay datos similares en la base de datos, cuando se insertan múltiples solicitudes simultáneamente, es posible que todos los hilos devuelvan que no hay datos similares, por lo que todos pueden unirse. En este momento, se necesita un procesamiento de sincronización, pero el bloqueo de la tabla de la base de datos es demasiado lento, por lo que se utiliza el bloqueo distribuido de Redis, que permite que solo un hilo realice la operación de inserción de datos, y los otros hilos esperan.
Este es el contenido completo de este artículo sobre la forma correcta de implementar el mecanismo de bloqueo distribuido de Redis en el lenguaje Java, espero que sea útil para todos. Los amigos interesados pueden continuar leyendo otros temas relacionados en este sitio, y si hay deficiencias, por favor déjenos un mensaje. Gracias a todos por su apoyo a este sitio!
Declaración: Este artículo se comparte en línea, es propiedad del autor original, el contenido se carga de manera autónoma por los usuarios de Internet, este sitio web no posee los derechos de propiedad, no se ha realizado un procesamiento editorial humano y no asume la responsabilidad legal correspondiente. Si encuentra contenido sospechoso de copyright, por favor envíe un correo electrónico a: notice#w3Declaración: El contenido de este artículo se obtiene de la red, es propiedad del autor original, el contenido se contribuye y carga de manera autónoma por los usuarios de Internet, este sitio web no posee los derechos de propiedad, no se ha realizado un procesamiento editorial humano y no asume la responsabilidad legal correspondiente. Si encuentra contenido sospechoso de copyright, por favor envíe un correo electrónico a: notice#w proporcionando la evidencia relevante, una vez confirmado, este sitio eliminará inmediatamente el contenido sospechoso de infracción.