¿Cómo implementar el bloqueo para garantizar la coherencia de los datos en condiciones de concurrencia? | Equipo técnico de JD Cloud

Plan de implementación de bloqueo bajo arquitectura monolítica.

1. Bloqueo global ReentrantLock

ReentrantLock (bloqueo reentrante) se refiere a una solicitud reentrante que tendrá éxito cuando un subproceso acceda nuevamente a un recurso crítico protegido por el bloqueo que contiene.

Simplemente compárelo con nuestro sincronizado de uso común:

  Bloqueo reentrante sincronizado
Mecanismo de implementación de bloqueo Depende de AQS modo monitor
flexibilidad Admite tiempos de espera de respuesta, interrupciones e intentos de adquirir bloqueos no flexible
forma de liberación Debes llamar explícitamente a unlock() para liberar el bloqueo. Monitor de liberación automática
Tipo de bloqueo Bloqueo justo y bloqueo injusto bloqueo injusto
cola condicional Se puede asociar con múltiples colas de condiciones Asociar una cola condicional
Reentrada reentrante reentrante

Mecanismo AQS: si el recurso compartido solicitado está inactivo, entonces el subproceso que actualmente solicita el recurso se configura como un subproceso de trabajo válido y el recurso compartido se pasa a través de CAScompareAndSetStateEstablecido en estado bloqueado; si el recurso compartido está ocupado, se utiliza un determinado mecanismo de activación de bloqueo y espera (cola FIFO de doble extremo de la variante CLH) para garantizar la asignación del bloqueo.

Reentrada: ya sea un bloqueo justo o injusto, el proceso de bloqueo utilizará un valor de estado

private volatile int state

  • El valor del estado es 0 cuando se inicializa, lo que indica que ningún subproceso retiene el bloqueo.
  • Cuando un hilo solicita el bloqueo, el valor del estado aumentará en 1. Si el mismo hilo adquiere el bloqueo varias veces, aumentará en 1 varias veces. Este es el concepto de reentrada.
  • El desbloqueo también disminuye el valor del estado en 1 hasta que llega a 0, y este hilo libera el bloqueo.
public class LockExample {

    static int count = 0;
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {

                try {
                    // 加锁
                    lock.lock();
                    for (int i = 0; i < 10000; i++) {
                        count++;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                finally {
                    // 解锁,放在finally子句中,保证锁的释放
                    lock.unlock();
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("count: " + count);
    }
}

/**
 * 输出
 * count: 20000
 */

2. Bloqueo de fila de Mysql, bloqueo optimista

El bloqueo optimista es una idea sin bloqueo, que generalmente se implementa en base a la idea CAS. En MySQL, el bloqueo optimista se implementa mediante el número de versión + forma sin bloqueo CAS; por ejemplo, cuando se ejecutan dos transacciones T1 y T2 al mismo tiempo, cuando se ejecuta la transacción T2 Después de un envío exitoso, la versión será +1, por lo que no se puede establecer la condición de versión para la ejecución de la transacción T1.

Bloquear declaraciones SQL y máquinas de estado operativo también puede evitar inconsistencias de datos causadas por el acceso simultáneo al valor de recuento por parte de diferentes subprocesos.

// 乐观锁 + 状态机
update
    table_name
set
    version = version + 1,
    count = count + 1
where
    id = id AND version = version AND count = [修改前的count值];

// 行锁 + 状态机
 update
    table_name
set
    count = count + 1
where
    id = id AND count = [修改前的count值]
for update;

3. Bloqueo ReetrantLock de grano fino

Si usamos directamente ReentrantLock para bloquear globalmente, entonces en este caso un subproceso adquiere el bloqueo y todos los subprocesos en todo el programa se bloquearán cuando lleguen aquí; pero en el proyecto queremos implementar una lógica de exclusión mutua para cada usuario durante la operación. , por lo que necesitamos cerraduras más finas.

public class LockExample {
    private static Map<String, Lock> lockMap = new ConcurrentHashMap<>();
    
    public static void lock(String userId) {
        // Map中添加细粒度的锁资源
        lockMap.putIfAbsent(userId, new ReentrantLock());
        // 从容器中拿锁并实现加锁
        lockMap.get(userId).lock();
    }
    public static void unlock(String userId) {
        // 先从容器中拿锁,确保锁的存在
        Lock locak = lockMap.get(userId);
        // 释放锁
        lock.unlock();
    }
}

Desventajas: Si cada usuario solicita un recurso compartido, se bloqueará una vez. El usuario no volverá a iniciar sesión en la plataforma, pero el objeto de bloqueo siempre existirá en el memoria Esto es equivalente a una pérdida de memoria, por lo que es necesario implementar el mecanismo de eliminación y tiempo de espera de bloqueo.

4. Bloqueo global sincronizado detallado

El mecanismo de bloqueo anterior utiliza un contenedor de bloqueoConcurrentHashMap. Por razones de seguridad de subprocesos, la mayor parte de la capa inferior seguirá utilizando el mecanismo Synchronized. Entonces, en algunos casos, usar lockMap requiere agregar dos capas de bloqueos.

Entonces, ¿podemos usar directamenteSynchronized para implementar un mecanismo de bloqueo detallado

public class LockExample {
    public static void syncFunc1(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            // 模拟业务耗时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "释放锁了");
        }
    }

    public static void syncFunc2(Long accountId) {
        String lock = new String(accountId + "").intern();

        synchronized (lock) {

            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            // 模拟业务耗时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "释放锁了");
        }
    }

    // 使用 Synchronized 来实现更加细粒度的锁
    public static void main(String[] args) {
        new Thread(()-> syncFunc1(123456L), "Thread-1").start();
        new Thread(()-> syncFunc2(123456L), "Thread-2").start();
    }
}

/**
 * 打印
 * Thread-1拿到锁了
 * Thread-1释放锁了
 * Thread-2拿到锁了
 * Thread-2释放锁了
 */

  • Del código, encontramos que el objeto que implementa el bloqueo es en realidad un objeto de cadena relacionado con la ID del usuario. Puede haber preguntas aquí. Cada vez que entra un nuevo hilo, el nuevo es un nuevo objeto de cadena, pero si la cadena Los contenidos son los mismos, ¿cómo podemos garantizar que los recursos compartidos se puedan bloquear de forma segura?
  • Esto en realidad debe atribuirse a la función de la siguienteintern() función;
  • intern()La función se utiliza para agregar una cadena al grupo de constantes de cadena en el espacio del montón en tiempo de ejecución. Si la cadena ya existe, devuelve una referencia al grupo de constantes de cadena.

Plan de implementación de bloqueo bajo arquitectura distribuida.

Problema central: Necesitamos encontrar un área visible para todos los subprocesos entre múltiples procesos para definir este mutex.

Una excelente solución de implementación de bloqueo distribuido debe cumplir con las siguientes características:

  1. En un entorno distribuido, se puede garantizar la exclusión mutua de subprocesos entre diferentes procesos.
  2. Al mismo tiempo, solo se permite que un subproceso adquiera con éxito el recurso de bloqueo.
  3. Cuando se garantizan mutex, es necesario garantizar una alta disponibilidad
  4. Para garantizar que las cerraduras se puedan adquirir y liberar con un alto rendimiento
  5. Puede admitir la reentrada de bloqueo para el mismo hilo
  6. Tenga un mecanismo de bloqueo razonable y los subprocesos que no puedan competir por los bloqueos deben tener las soluciones correspondientes.
  7. Admite la adquisición de bloqueos sin bloqueo. Los subprocesos que no logran adquirir el bloqueo pueden regresar directamente
  8. Tener un mecanismo de falla de bloqueo razonable, como falla de tiempo de espera, etc., puede garantizar que se eviten situaciones de bloqueo.

Redis implementa bloqueo distribuido

  • Redis es middleware y se puede implementar de forma independiente;
  • Es visible para diferentes procesos Java y el rendimiento también es muy impresionante.
  • se basa en las instrucciones proporcionadas por el propio redissetnx key value para implementar bloqueos distribuidos; la diferencia con las instrucciones ordinarias set es que solo ocurrirá cuando el la clave no existe La configuración se realizó correctamente. Si la clave existe, se devolverá un error de configuración

Ejemplo de código:

// 扣库存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // 获取锁
    String lockKey = "lock-" + inventory.getInventoryId();
    int timeOut = 100;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "竹子-熊猫",timeOut,TimeUnit.SECONDS);
    // 加上过期时间,可以保证死锁也会在一定时间内释放锁
    stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
    
    if(!flag){
        // 非阻塞式实现
        return "服务器繁忙...请稍后重试!!!";
    }
    
    // ----只有获取锁成功才能执行下述的减库存业务----        
    try{
        // 查询库存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "库存不足,请联系卖家....";
        }
        
        // 扣减库存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
    } catch (Exception e) { // 确保业务出现异常也可以释放锁,避免死锁
        // 释放锁
        stringRedisTemplate.delete(lockKey);
    }
    
    if (n > 0)
        return "端口-" + port + ",库存扣减成功!!!";
    return "端口-" + port + ",库存扣减失败!!!";
}

作者:竹子爱熊猫
链接:https://juejin.cn/post/7038473714970656775

Análisis razonable del tiempo de vencimiento:

Porque para diferentes negocios, la duración del tiempo de vencimiento que establecemos será diferente: si es demasiado largo, es inapropiado, y si es demasiado corto, es inapropiado;

Entonces, la solución que se nos ocurrió fue configurar un subproceso para extender la vida útil del recurso de bloqueo actual. La implementación específica es que el subproceso verifica si la clave ha caducado cada 2-3 segundos. Si no ha caducado, significa que el subproceso empresarial todavía está ejecutando el negocio, luego se agregan 5 segundos al tiempo de caducidad de la llave.

Sin embargo, para evitar que el hilo principal muera accidentalmente, el hilo secundario continuará viviendo para él, lo que provocará el fenómeno de "bloqueo de inmortalidad", por lo que el hilo secundario se convierte en el hilo demonio del hilo principal (negocio). hilo, de modo que el hilo secundario siga al hilo principal.

// 续命子线程
public class GuardThread extends Thread { 
    private static boolean flag = true;

    public GuardThread(String lockKey, 
        int timeOut, StringRedisTemplate stringRedisTemplate){
        ……
    }

    @Override
    public void run() {
        // 开启循环续命
        while (flag){
            try {
                // 先休眠一半的时间
                Thread.sleep(timeOut / 2 * 1000);
            }catch (Exception e){
                e.printStackTrace();
            }
            // 时间过了一半之后再去续命
            // 先查看key是否过期
            Long expire = stringRedisTemplate.getExpire(
                lockKey, TimeUnit.SECONDS);
            // 如果过期了,代表主线程释放了锁
            if (expire <= 0){
                // 停止循环
                flag = false;
            }
            // 如果还未过期
            // 再为则续命一半的时间
            stringRedisTemplate.expire(lockKey,expire
                + timeOut/2,TimeUnit.SECONDS);
        }
    }
}


// 创建子线程为锁续命
GuardThread guardThread = new GuardThread(lockKey,timeOut,stringRedisTemplate);
// 设置为当前 业务线程 的守护线程
guardThread.setDaemon(true);
guardThread.start();

作者:竹子爱熊猫 
链接:https://juejin.cn/post/7038473714970656775

El problema de la falla de bloqueo en la arquitectura maestro-esclavo de Redis

Para garantizar la alta disponibilidad de Redis durante el proceso de desarrollo, se utilizará la arquitectura de replicación maestro-esclavo para separar la lectura y la escritura, mejorando así el rendimiento y la disponibilidad de Redis. Sin embargo, si un hilo adquiere con éxito el bloqueo en el nodo maestro de Redis, el nodo maestro falla antes de que tenga tiempo de copiar al nodo esclavo. En este momento, otro hilo accede a Redis y accederá al nodo esclavo, y al mismo tiempo adquiere el bloqueo exitosamente. Estos problemas de seguridad pueden surgir al acceder a recursos críticos.

Solución:

  • Algoritmo de bloqueo rojo (solución oficial): Múltiples Redis independientes escriben datos al mismo tiempo. Dentro del tiempo de vencimiento del bloqueo, si más de la mitad de las máquinas escriben con éxito, se devolverá el éxito de adquisición del bloqueo. Cuando fallan, las máquinas exitosas será liberado bloqueo. Sin embargo, la desventaja de este enfoque es que es costoso y requiere la implementación independiente de múltiples nodos de Redis.
  • Registro adicional del estado del bloqueo: el estado del bloqueo se registra adicionalmente a través de otro middleware implementado de forma independiente (como DB). Antes de que el nuevo subproceso adquiera el bloqueo, debe consultar el registro de retención de bloqueo en la base de datos. el estado del bloqueo es Intente adquirir el bloqueo distribuido nuevamente cuando no esté retenido. PeroLas desventajas de esta situación son obvias. El proceso de adquisición del candado es difícil de implementar y la sobrecarga de rendimiento también es muy alta; además, Es necesario actualizar los datos en la base de datos con la función de temporizador. El estado del bloqueo garantiza el mecanismo de falla razonable del bloqueo.
  • Implementado usando Zookepper

Zookeeper implementa bloqueos distribuidos

Los datos de Zookeeper son diferentes de los datos de Redis. Los datos se sincronizan en tiempo real. Después de que el nodo maestro escribe, más de la mitad de los nodos deben escribir antes de que regrese con éxito. Por lo tanto, si proyectos como el comercio electrónico y la educación persiguen un alto rendimiento, pueden renunciar a un cierto grado de estabilidad, y se recomienda utilizar redis; por ejemplo, proyectos como finanzas, banca, gobierno, etc., persiguen un alto estabilidad y puede sacrificar parte del rendimiento. Se recomienda Implementado usando Zookeeper.

Optimización del rendimiento de bloqueo distribuido

De hecho, el bloqueo anterior resuelve el problema de la seguridad de los subprocesos en situaciones de concurrencia, pero ¿cómo deberíamos resolver el escenario en el que 1 millón de usuarios se apresuran a comprar 1000 productos al mismo tiempo?

  • Puede preparar los recursos compartidos con anticipación y almacenar una copia en segmentos. La hora punta de compra es a las 15:00 de la tarde, la cantidad de mercancías se dividirá en 10 partes alrededor de las 14:30 de antelación y cada dato se bloqueará por separado para evitar anomalías de concurrencia.
  • Además, se deben escribir 10 claves en redis. Cada nuevo subproceso ingresa y asigna aleatoriamente un bloqueo, y luego realiza la lógica de reducción de inventario posterior. Una vez completado, el bloqueo se libera para que lo utilicen los subprocesos posteriores.
  • La idea de este tipo de bloqueo distribuido es implementar la función de acceso sincrónico de subprocesos múltiples a recursos compartidos que se puede implementar originalmente con un bloqueo. Para mejorar la velocidad de acceso de subprocesos múltiples en condiciones instantáneas, es También es necesario garantizar la concurrencia y la seguridad.

Artículo de referencia:

  1. https://juejin.cn/post/7236213437800890423

  2. https://juejin.cn/post/7038473714970656775

  3. https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

Autor: JD Technology Jiao Zebin

Fuente: Comunidad de desarrolladores de JD Cloud Indique la fuente al reimprimir

Actualización anual de la versión principal de IntelliJ IDEA 2023.3 y JetBrains Nuevo concepto de "programación defensiva": conviértase en un trabajo estable GitHub .com ejecuta más de 1200 hosts MySQL, ¿cómo actualizar sin problemas a 8.0? El equipo Web3 de Stephen Chow lanzará una aplicación independiente el próximo mes ¿Se eliminará Firefox? Visual Studio Code 1.85 lanzado, ventana flotante Yu Chengdong: Huawei lanzará productos disruptivos el próximo año y reescribirá la historia de la industria EE.UU. CISA recomienda abandonar C/C++ y eliminar las vulnerabilidades de seguridad de la memoria TIOBE Diciembre: se espera que C# se convierta en el lenguaje de programación del año Artículo escrito por Lei Jun Hace 30 años: "Principios y diseño del sistema experto para la determinación de virus informáticos
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/4090830/blog/10319213
Recomendado
Clasificación