Realización y Aplicación de Bloqueo Distribuido

¿Por qué necesitas un candado?

Resolución de problemas de competencia de datos en escenarios concurrentes en un entorno multitarea

Bloqueo común de Java

Podemos agrupar y clasificar según si la cerradura contiene una determinada característica

  • Si el subproceso bloquea el recurso, el bloqueo se puede dividir en bloqueo optimista y bloqueo pesimista
  • Si el subproceso está bloqueado cuando el recurso esclavo está bloqueado, se puede dividir en bloqueos de giro (familia atómica bajo JUC) y bloqueos de bloqueo (sincronizados, ReentrantLock)
  • El acceso simultáneo a los recursos de varios subprocesos se puede dividir en bloqueos sesgados sin bloqueo, bloqueos livianos y bloqueos pesados ​​(jdk1.6 comenzó a optimizar los bloqueos)
  • Distinguir de la equidad de las cerraduras, dividida en cerraduras justas y cerraduras injustas
  • Si el bloqueo se puede adquirir repetidamente se puede dividir en bloqueos reentrantes y bloqueos no reentrantes
  • Si varios subprocesos pueden obtener el mismo bloqueo se puede dividir en bloqueos compartidos y bloqueos exclusivos

inserte la descripción de la imagen aquí

¿Por qué necesita bloqueos distribuidos?

En un entorno de aplicación independiente, todos los subprocesos se ejecutan en el mismo proceso jvm, y usar el bloqueo que viene con Java es suficiente para controlar la simultaneidad, pero en un escenario distribuido, varios subprocesos se ejecutan en diferentes máquinas (procesos jvm). son necesarios para resolver el problema

¿Qué es un bloqueo distribuido?

Un bloqueo distribuido es una implementación de un bloqueo que controla el acceso simultáneo a recursos compartidos por diferentes procesos en un sistema distribuido. Si un recurso crítico (como los datos en una base de datos) se comparte entre diferentes hosts, a menudo se requiere la exclusión mutua para evitar la interferencia mutua y garantizar la coherencia.

Función: cuando varios servicios en un clúster distribuido solicitan el mismo método o la misma operación comercial (como seckill), la lógica comercial correspondiente solo puede ejecutarse mediante un subproceso en una máquina para evitar problemas de seguridad simultáneos.

Bloqueo distribuido basado en la implementación de la base de datos

Utilice select...for updatebloqueos de fila de base de datos para implementar bloqueos pesimistas. Nota: Si la condición de consulta utiliza una clave principal/índice , seleccione... para actualizar bloqueará la fila ; si es un campo ordinario (sin clave principal/índice), seleccione... para actualizar bloqueará la tabla .

Implementación de bloqueo pesimista

El método de bloqueo de adquisición debe declarar una transacción, agregar un bloqueo de fila de datos y liberar el bloqueo de fila cuando finaliza la transacción. La operación de liberación del bloqueo debe colocarse finalmente

El pseudocódigo es el siguiente:

//整体流程
try {
    
    
    if(lock(keyResource)){
    
    //加锁
        process();//业务逻辑处理
     }
} finally {
    
    
    unlock(keyResource);//释放
}

//锁方法实现
//获取锁
public boolean lock(String keyResource){
    
    
    resLock = 'select * from resource_lock where key_resource = '#{
    
    keySource}' for update';
    if(resLock != null && resLock.getLockFlag == 1){
    
    
        return false;
    }
    resLock.setLockFlag(1);//上锁
    insertOrUpdate(resLock);//提交
    return true;
}

//释放锁
public void unlock(String keyResource){
    
    
    resourceLock.setLockFlag(0);//解锁
    update(resourceLock);//提交
}

Implementación de bloqueo optimista

Basado en la idea de CAS, agregue un campo de versión a la tabla de la base de datos.

Al usar, actualice con condiciones (versión de evaluación)

mybatis-plus ya admite la configuración automática

características

  1. Debido al cuello de botella de rendimiento de la propia base de datos, el bloqueo distribuido basado en la base de datos se utiliza principalmente en escenarios con baja concurrencia
  2. El método de práctica es simple, estable y confiable.

Bloqueo distribuido basado en Redis

Plan original

Use SETNXel comando, SETNXa saber, SET si N ot e XsetIfAbsent ists (correspondiente al método en Java ), si la clave no existe, el valor de la clave se establecerá y se devolverá 1. Si la clave ya existe, SETNXno haga nada y devuelva 0.

expire KEY secondsEstablezca el tiempo de caducidad de la clave, si la clave ha caducado, se eliminará automáticamente.

del KEYeliminar clave

El pseudocódigo es el siguiente:

//setnx加锁
if(jedis.setnx(key,lock_value) == 1){
    
    
    //设定锁过期时间
    expire(key,10);
    try{
    
    
        //业务处理
        do();
    } catch(){
    
    
        
    }finally{
    
    
        //释放锁
        jedis.del(key);
    }
}

En este esquema original, setnx y expire son dos operaciones separadas en lugar de operaciones atómicas . Si después de ejecutar la operación setnx, el proceso se cuelga antes de ejecutar expire para establecer el tiempo de vencimiento, entonces el bloqueo no se puede liberar y otros subprocesos no pueden adquirir el bloqueo.

Comando de extensión SET (después de la versión Redis2.6.12)

Usar el comando de extensión redisSET key value[EX seconds][PX milliseconds][NX|XX]

El significado de cada parámetro es el siguiente:

  • key: El nombre de la clave a configurar.
  • value: El valor a establecer.
  • EX seconds: un parámetro opcional que indica el tiempo de caducidad de la clave establecida en segundos.
  • PX milliseconds: un parámetro opcional que indica el tiempo de caducidad de la clave establecida en milisegundos.
  • NX: Parámetro opcional, lo que significa establecer el valor solo si la clave no existe.
  • XX: parámetro opcional, lo que significa establecer el valor solo si la clave ya existe.

Los ejemplos son los siguientes:

  • Establezca un par clave-valor sin opciones:
SET username alice

Esto establecerá la clave llamada "nombre de usuario" en el valor "alicia".

  • Establezca un par clave-valor con un tiempo de vencimiento:
SET session_token 123456 EX 3600

Esto establecerá el valor de la clave denominada "session_token" en "123456" y la clave caducará después de 3600 segundos (1 hora).

  • Establezca un par clave-valor con un tiempo de caducidad en milisegundos:
SET cache_key data123 PX 5000

Esto establecerá el valor de la clave denominada "cache_key" en "data123" y la clave caducará después de 5000 milisegundos (5 segundos).

  • Solo establezca el valor si la clave no existe:
SET order_status pending NX

Si la clave "order_status" no existe, se establecerá en "pendiente". Si la clave ya existe, no haga nada.

  • Solo establezca el valor si la clave ya existe:
SET login_attempts 3 XX

Si la clave "login_attempts" ya existe, su valor se establecerá en "3". Si el nombre de la clave no existe, no haga nada.

Después de entender este comando, podemos usarlo para construir un bloqueo distribuido

El pseudocódigo es el siguiente:

if(jedis.set(key,lock_value,"NX","EX",10s) == 1){
    
    
    try{
    
    
        do();
    }catch(){
    
    
  
    }finally{
    
    
        jedis.del(key);
    }
}

Esta operación garantiza la atomicidad de set y expire, pero todavía hay otros problemas:

  1. El bloqueo caducó y se liberó, pero el negocio aún no se ha ejecutado (la solución se mencionará más adelante: mecanismo de vigilancia)
  2. El bloqueo es eliminado accidentalmente por otros subprocesos (la solución se mencionará más adelante: Lua script): después de que el bloqueo del subproceso 1 caduca y se libera, es adquirido por otros subprocesos (subproceso 2), pero el subproceso anterior (subproceso 1) es del nuevamente después del bloqueo de ejecución (es decir, se libera el bloqueo del subproceso 2), en el caso de alta concurrencia, este escenario es equivalente a ningún bloqueo

El problema de la eliminación accidental de bloqueos

Algunos estudiantes preguntarán, dado que el bloqueo puede ser eliminado accidentalmente por otros subprocesos, ¿podemos agregarle un identificador único? En términos generales, no hay problema en pensar, pero no se puede procesar simplemente en Java

Si juzgamos en Java, el pseudocódigo es el siguiente:

//加锁时设置一个随机id来作为标识,如果释放锁时还是这个id即证明释放了自己的锁(实际上是有逻辑错误的)
if(jedis.set(key,randomId,"NX","EX",10s) == 1){
    
    
    try{
    
    
        do();
    }catch(){
    
    
  
    }finally{
    
    
        //从redis获取randomId,如果是期望值则释放
        if(randomId.equals(jedis.get(key))){
    
    
        	jedis.del(key);
        }
    }
}

Parece que no hay problema, pero aquí habrá un problema que no es causado por operaciones atómicas : si el bloqueo caduca después de que se considera que randomId es el valor esperado, el segundo subproceso crea su propio bloqueo. porque el primer subproceso ya ha Si se pasa el juicio de randomId, aún liberará el bloqueo recién creado por el subproceso 2, y el problema de la eliminación accidental del bloqueo aún existe ...

La buena noticia es que tenemos otras soluciones.

Después de la versión 2.6 de redis, los desarrolladores pueden usar Lua para escribir scripts y pasarlos a redis para su ejecución. Los beneficios de hacerlo son los siguientes:

  1. Reduzca la sobrecarga de la red: la operación de múltiples solicitudes de red se puede completar con una solicitud, y la lógica de las múltiples solicitudes originales se completa en el servidor redis. El uso de scripts reduce el retraso de ida y vuelta de la red;
  2. Operación atómica: Redis ejecutará todo el script como un todo y no será insertado/interrumpido por otros comandos en el medio;
  3. Reemplace la función de transacción de redis: el script Lua de Redis casi realiza la función de transacción convencional y admite el informe de errores y las operaciones de reversión. Se recomienda oficialmente que si desea utilizar la función de transacción de redis, puede usar el script lua de redis en su lugar. .

La sintaxis básica del comando Redis Eval es la siguiente

EVAL script numkeys key [key ...] arg [arg ...] 

#实例			   eval  引号中是脚本内容                         key的个数 key[...]  arg[...]  
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"

En este momento, podemos usar scripts de Lua para garantizar la atomicidad de las operaciones.

guion lua:

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

En redis:

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

En Java:

String key = "key";
String value = "value";
// 定义 Lua 脚本
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 执行 Lua 脚本
Object result = jedis.eval(luaScript, 1, key, value);

En este punto podemos solucionar el problema de borrar accidentalmente el bloqueo, pero hay otro problema que no se ha solucionado ¿Qué debo hacer si el bloqueo caduca y se libera pero el negocio aún no se ha ejecutado?

El mecanismo de vigilancia de Redisson

Redisson es similar a Jedis. Es un cliente para Java para operar Redis. Es más útil que Jedis para resolver escenarios distribuidos. Proporciona varios objetos distribuidos, bloqueos distribuidos, sincronizadores distribuidos, servicios distribuidos, etc.

El proceso de implementación del bloqueo distribuido Redission es el siguiente

El código fuente de la realización de la renovación automática de Redisson es el siguiente:

private void renewExpiration() {
    
    
  // 获取当前锁的过期时间续约条目
  RedissonBaseLock.ExpirationEntry ee = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(this.getEntryName());

  // 如果存在续约条目
  if (ee != null) {
    
    
    // 创建定时任务,定时执行续约操作
    Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
    
    
      public void run(Timeout timeout) throws Exception {
    
    
        // 获取续约条目
        RedissonBaseLock.ExpirationEntry ent = (RedissonBaseLock.ExpirationEntry) EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());

        // 如果续约条目存在
        if (ent != null) {
    
    
          Long threadId = ent.getFirstThreadId();

          // 如果存在线程ID
          if (threadId != null) {
    
    
            // 异步执行续约操作
            CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);

            // 当异步操作完成时
            future.whenComplete((res, e) -> {
    
    
              if (e != null) {
    
    
                // 发生错误时,记录日志并移除续约条目
                RedissonBaseLock.log.error("Can't update lock " + RedissonBaseLock.this.getRawName() + " expiration", e);
                RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
              } else {
    
    
                // 如果续约成功,递归调用续约操作
                if (res) {
    
    
                  RedissonBaseLock.this.renewExpiration();
                } else {
    
    
                  // 如果无法续约,取消续约操作
                  RedissonBaseLock.this.cancelExpirationRenewal((Long)null);
                }
              }
            });
          }
        }
      }
    }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);

    // 设置定时任务到续约条目
    ee.setTimeout(task);
  }
}

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    
    
  // 使用 evalWriteAsync 方法执行 Lua 脚本
  // 这个脚本会检查锁是否仍然由给定的线程持有,如果是则更新锁的过期时间
  return this.evalWriteAsync(
    this.getRawName(),                       // 锁的键名
    LongCodec.INSTANCE,                      // 键的编码器
    RedisCommands.EVAL_BOOLEAN,              // 使用 EVAL 命令并返回布尔值
    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;",
    Collections.singletonList(this.getRawName()),     // 键名作为 KEYS[1]
    this.internalLockLeaseTime,              // 锁的过期时间(毫秒)
    this.getLockName(threadId)               // 获取锁的名称,用于验证锁的持有者
  );
}

Resumir:

  1. El perro guardián solo se usa si no se especifica un tiempo de espera de bloqueo
  2. Si la instancia de Redisson cuelga, el perro guardián también se bloqueará, entonces redis borrará la clave que ha alcanzado el tiempo de caducidad, y el bloqueo se liberará, y no habrá problema de que el bloqueo esté ocupado permanentemente.

La interfaz RLock de Redisson hereda la interfaz de bloqueo de JUC, por lo que se ajusta a la especificación de la interfaz de bloqueo en Java. Al mismo tiempo, Redisson también proporciona una variedad de clases de implementación de bloqueo distribuido (por ejemplo: RedissonFairLock, RedissonRedLock, etc.) para que usted elija

Problema de inconsistencia de datos del clúster de Redis

Al implementar redis, para evitar problemas de un solo punto, generalmente implementamos en un modo de clúster . Dado que la sincronización de datos del clúster de redis es una operación asíncrona, devolverá el bloqueo exitoso después de que el nodo maestro esté bloqueado; si un hilo obtiene el bloqueo en el nodo maestro Cuando se alcanza el bloqueo, pero la clave bloqueada no se ha sincronizado con el nodo esclavo, el nodo maestro falla . Un nodo esclavo se actualizará a un nodo maestro, y otros subprocesos también pueden adquirir el bloqueo de la misma clave, lo que equivale a no agregar Lock

El autor de redis propuso un algoritmo de bloqueo distribuido avanzado: Redlock, para resolver este problema

Idea central de Redlock

Realice varias implementaciones maestras de Redis para asegurarse de que no se caigan al mismo tiempo. Y estos nodos maestros son completamente independientes entre sí , y no hay sincronización de datos entre ellos. Al mismo tiempo, debe asegurarse de que en estas múltiples instancias maestras use el mismo método que la instancia única de Redis para adquirir y liberar bloqueos.

inserte la descripción de la imagen aquí

Pasos del proceso Redlock

1. Solicite bloqueos de múltiples nodos maestros (como se muestra en la figura 5 anterior) en secuencia

2. A juzgar de acuerdo con el período de tiempo de espera establecido, si se salta el nodo maestro;

3. Si más de la mitad de los nodos se bloquean con éxito (los tres en la imagen de la derecha son suficientes) y el tiempo de uso es menor que el período de validez del bloqueo (establezca un tiempo de espera de un solo nodo), el bloqueo se puede considerar exitoso ;

4. Si falla la adquisición del bloqueo, desbloquee todos los nodos maestros

Bloqueo distribuido basado en ZooKeeper

He escrito un blog sobre esto antes, por lo que puede hacer clic en el enlace para saltar ~

Implementación de bloqueo distribuido basada en nodos secuenciales temporales de ZooKeeper_❀always❀'s Blog-CSDN Blog

La idea de implementación es la siguiente:

inserte la descripción de la imagen aquí

Comparación de esquemas de implementación de bloqueo distribuido

plan tren de pensamiento ventaja defecto escena tipica
mysql cerradura pesimista, cerradura optimista Sencillo, estable y fiable. Bajo rendimiento, no apto para alta concurrencia Tareas de temporización distribuidas
redis Atomicidad garantizada de las operaciones de caché basadas en scripts SETNX y Lua Buen desempeño (AP) Relativamente complicado de implementar, no 100% confiable Ventas relámpago, compras rápidas y sorteos a gran escala
cuidador del zoológico Características del nodo basado en ZK y mecanismo Watcher Alta fiabilidad (CP) La implementación es relativamente compleja y el rendimiento es ligeramente peor. Ventas relámpago, compras rápidas y sorteos a gran escala

Bloqueos distribuidos y alta concurrencia

Desde el punto de vista del diseño, los bloqueos distribuidos y la alta concurrencia son contradictorios : los bloqueos distribuidos en realidad serializan códigos paralelos para resolver problemas de concurrencia, que tienen un impacto en el rendimiento, pero pueden optimizarse.

Los principales programas son:

  1. Granularidad de bloqueo más pequeña: coloque el código con la granularidad más pequeña que tenga problemas de seguridad de concurrencia en el bloqueo tanto como sea posible, y coloque otros códigos fuera del bloqueo. Este es el principio básico de optimización del bloqueo.
  2. Fragmentación de datos: por ejemplo, ConcurrentHashMap utiliza un mecanismo de bloqueo segmentado para mejorar la concurrencia, la subbase de datos MySQL y la subtabla (distribución de presión a diferentes bases de datos), etc.

Aplicación de bloqueos distribuidos en escenarios empresariales

  • Después de que ocurre un evento, es necesario enviar un mensaje de texto para recordar al usuario, y si el evento ocurre varias veces dentro de dos horas, solo se le recuerda al usuario la primera vez.

    Idea de implementación: obtenga el bloqueo distribuido antes de enviar un mensaje de texto cada vez, establezca el tiempo de caducidad en 2 h, si el evento vuelve a ocurrir dentro de 2 h, no se puede obtener el mismo bloqueo distribuido y el proceso de envío del mensaje de texto se puede omitir automáticamente

  • Asegúrese de que solo haya una pieza de datos identificada de forma única por id+hora del día en una tabla

    Idea de implementación: al insertar o actualizar, primero obtenga el bloqueo distribuido y desbloquéelo después de una inserción exitosa

  • Consigue una cantidad limitada de premios

    Idea de implementación: Cada subproceso se adelanta al bloqueo distribuido. Después de que la preferencia tiene éxito, se juzga si la cantidad restante cumple con la cantidad requerida. Si es así, la preferencia tiene éxito y se libera el bloqueo.

Supongo que te gusta

Origin blog.csdn.net/m0_51561690/article/details/132285525
Recomendado
Clasificación