Hablando de bloqueo pesimista y bloqueo optimista

1. ¿Por qué ocurren bloqueos pesimistas y bloqueos optimistas?

Cuando pueda haber concurrencia en el programa, es necesario asegurar la exactitud de los datos en condiciones concurrentes, de modo que cuando el usuario actual opere junto con otros usuarios, los resultados obtenidos sean los mismos que los obtenidos cuando opere. solo. Esto se llama control de concurrencia. El propósito del control de concurrencia es asegurar que el trabajo de un usuario no afecte irrazonablemente el trabajo de otro usuario.
Sin un buen control de concurrencia, pueden ocurrir problemas como lecturas sucias, lecturas fantasma y lecturas no repetibles.
El control de concurrencia que se suele decir está generalmente relacionado con el sistema de gestión de bases de datos (DBMS). La tarea del control de concurrencia en DBMS es garantizar que cuando varias transacciones agregan, eliminan, modifican y consultan los mismos datos al mismo tiempo, el aislamiento, la coherencia y la unidad de la base de datos de las transacciones no se destruyen.
Los medios principales para lograr el control de concurrencia se dividen en control de concurrencia optimista y control de concurrencia pesimista.
Tanto los bloqueos pesimistas como los bloqueos optimistas son conceptos definidos por los desarrolladores y pueden considerarse como ideas. De hecho, no solo existen conceptos de bloqueo optimista y bloqueo pesimista en los sistemas de bases de datos relacionales, sino también conceptos similares como hibernate, tair y memcache. Por lo tanto, no se deben comparar el bloqueo optimista, el bloqueo pesimista y otros bloqueos de bases de datos.

  • El bloqueo optimista es más adecuado para leer más y escribir menos ( escenarios de lectura múltiple )
  • El bloqueo pesimista es más adecuado para escribir más y leer menos ( múltiples escenarios de escritura ).

inserte la descripción de la imagen aquí

Antes de explicar el bloqueo pesimista y el bloqueo optimista, debemos comprender brevemente las lecturas sucias, las lecturas fantasma y las lecturas no repetibles.

2. Lecturas sucias, lecturas fantasma y lecturas no repetibles

2.1 Lecturas sucias

Durante la ejecución de la transacción A, la transacción B lee la modificación de la transacción A. Sin embargo, debido a algunas razones, es posible que la transacción A no complete el envío y, si se produce una operación de reversión, los datos leídos por la transacción B serán incorrectos. Estos datos no confirmados son una lectura sucia. El proceso de generación de lectura sucia es el siguiente:
inserte la descripción de la imagen aquí

2.2 Lectura fantasma

La transacción B lee los datos dos veces y la transacción A agrega datos durante los dos procesos de lectura, y los dos conjuntos de lectura de la transacción B son diferentes. El proceso de lectura fantasma es el siguiente:
inserte la descripción de la imagen aquí

2.3 Lectura no repetible

La transacción B lee los datos dos veces. Durante los dos procesos de lectura, la transacción A modifica los datos y los datos leídos por la transacción B son diferentes. El resultado de esta lectura por la transacción B es Lectura no repetible. El proceso de generación de lecturas no repetibles es el siguiente:
inserte la descripción de la imagen aquí

Podemos ver que los conceptos de lectura no repetible y lectura fantasma son muy similares, pero la diferencia esencial entre los dos es que la lectura no repetible es la modificación de datos, y es diferente de los datos originales cuando se lee de nuevo, y lectura fantasma se refiere a la lectura El número de conjuntos es diferente. Tal vez se adquieran 12 datos por primera vez, y un dato se elimina por algún motivo al leer la segunda vez, lo que da como resultado que solo se lean 9 datos. como si este dato nunca hubiera sido leído.Lo mismo sucedió.

2.4 Nivel de aislamiento de la base de datos

Leer sin compromiso

En este nivel de aislamiento, todas las transacciones pueden ver los resultados de ejecución de otras transacciones no confirmadas. Este nivel de aislamiento es el nivel de aislamiento más bajo. Aunque tiene una capacidad de procesamiento concurrente muy alta y una sobrecarga del sistema baja, rara vez se usa en aplicaciones prácticas. Debido a que este nivel de aislamiento solo puede prevenir el primer tipo de problema de pérdida de actualización, no puede resolver los problemas de lecturas sucias, lecturas no repetibles y lecturas fantasma.

Lectura comprometida

Este es el nivel de aislamiento predeterminado para la mayoría de los sistemas de bases de datos (pero no el predeterminado de MySQL). Satisface la definición simple de aislamiento: una transacción solo puede ver los cambios realizados por transacciones confirmadas.
Este nivel de aislamiento puede evitar problemas de lectura sucia, pero habrá lecturas no repetibles y lecturas fantasma.

Lectura repetible

Este es el nivel de aislamiento de transacciones predeterminado de MySQL, que garantiza que varias instancias de la misma transacción verán las mismas filas de datos al leer datos simultáneamente. Este nivel de aislamiento evita otros problemas además de las lecturas fantasma .

Serializable

Este es el nivel de aislamiento más alto, que resuelve el problema de las lecturas fantasma y las actualizaciones perdidas del segundo tipo al forzar el orden de las transacciones para que no puedan entrar en conflicto entre sí. En este nivel, todos los problemas de concurrencia mencionados anteriormente se pueden resolver, pero puede conducir a una gran cantidad de tiempos de espera y competencia de bloqueo. Por lo general, la base de datos no utilizará este nivel de aislamiento. Necesitamos otros mecanismos para resolver estos problemas: bloqueo optimista y bloqueo pesimista.
inserte la descripción de la imagen aquí

3. Bloqueo pesimista

Al modificar un dato en la base de datos, para evitar que otros lo modifiquen al mismo tiempo, la mejor manera es bloquear directamente los datos para evitar la concurrencia.
Este método de bloqueo antes de modificar los datos con la ayuda del mecanismo de bloqueo de la base de datos se denomina control de concurrencia pesimista.

Bloqueo pesimista, con fuertes características exclusivas y exclusivas . Se refiere a una actitud conservadora hacia los datos modificados por el mundo exterior (incluidas otras transacciones actuales en el sistema, así como transacciones de sistemas externos). Por lo tanto, durante todo el proceso de procesamiento de datos, los datos se mantienen bloqueados.

3.1 Implementación del bloqueo pesimista

La implementación de bloqueos pesimistas a menudo se basa en el mecanismo de bloqueo proporcionado por la base de datos (y solo el mecanismo de bloqueo proporcionado por la capa de la base de datos puede garantizar verdaderamente la exclusividad del acceso a los datos. De lo contrario, incluso si el mecanismo de bloqueo se implementa en este sistema, no puede se garantiza que el sistema externo no se modificará.
El proceso general de bloqueo pesimista es el siguiente:
inserte la descripción de la imagen aquí

La razón por la que se llama bloqueo pesimista es porque es un método de control de concurrencia con una actitud pesimista hacia la modificación de datos. Asuma siempre el peor de los casos, cada vez que se leen los datos, otros subprocesos cambiarán los datos de forma predeterminada, por lo que se requiere una operación de bloqueo, y cuando otros subprocesos desean acceder a los datos, deben bloquear y suspender. La implementación del bloqueo pesimista:

Implementaciones comunes de bloqueo pesimista:

  • Las bases de datos relacionales tradicionales utilizan este mecanismo de bloqueo, como bloqueos de fila, bloqueos de tabla, bloqueos de lectura y bloqueos de escritura, todos los cuales se bloquean antes de las operaciones.
  • La implementación de la palabra clave sincronizada en Java.

Los bloqueos pesimistas se dividen principalmente en bloqueos compartidos y bloqueos exclusivos:

  • 1. Los bloqueos compartidos también se conocen como bloqueos de lectura o bloqueos S para abreviar. Como su nombre lo indica, un bloqueo compartido significa que varias transacciones pueden compartir un bloqueo en los mismos datos y pueden acceder a los datos, pero solo pueden leerlos y no modificarlos .
  • 2. Los bloqueos exclusivos [bloqueos exclusivos] también se conocen como bloqueos de escritura o bloqueos X para abreviar. Como su nombre lo indica, un bloqueo exclusivo no puede coexistir con otros bloqueos. Si una transacción adquiere un bloqueo exclusivo en una fila de datos, otras transacciones no pueden adquirir otros bloqueos en la fila, incluidos los bloqueos compartidos y los bloqueos exclusivos. Una transacción que adquiere un bloqueo exclusivo puede leer y modificar filas de datos.

El control de concurrencia pesimista es en realidad una estrategia conservadora de " cerrar primero y luego acceder ", que proporciona una garantía para la seguridad del procesamiento de datos. Sin embargo, en términos de eficiencia, el mecanismo para manejar el bloqueo generará una sobrecarga adicional para la base de datos y aumentará la posibilidad de interbloqueos. Además, se reducirá el paralelismo.Si una transacción bloquea una fila de datos, otras transacciones deben esperar a que se complete la transacción antes de procesar esa fila de datos.

4. Bloqueo optimista

El bloqueo optimista es relativo al bloqueo pesimista. El bloqueo optimista supone que los datos no causarán conflictos en general, por lo que cuando los datos se envían y actualizan, el conflicto de datos se detectará formalmente. Si hay un conflicto, se devolverá una excepción al información del usuario, lo que permite a los usuarios decidir qué hacer. El bloqueo optimista es adecuado para escenarios con más lecturas y menos escrituras , lo que puede mejorar el rendimiento del programa.
inserte la descripción de la imagen aquí

El bloqueo optimista adopta un mecanismo de bloqueo más relajado. También es un mecanismo para evitar errores de procesamiento de datos causados ​​por la lectura fantasma de la base de datos y el tiempo prolongado de procesamiento comercial. Sin embargo, el bloqueo optimista no utiliza deliberadamente el mecanismo de bloqueo de la base de datos en sí, sino que garantiza la corrección de los datos en función de los datos mismos.

4.1 Implementación del bloqueo optimista

  • Implementación CAS: Las variables atómicas bajo el paquete java.util.concurrent.atomic en Java usan una implementación CAS de bloqueo optimista.
  • Control de número de versión: por lo general, se agrega un campo de versión de número de versión de datos a la tabla de datos para indicar la cantidad de veces que se han modificado los datos. Cuando se modifican los datos, el valor de la versión será +1. Cuando el subproceso A quiere actualizar los datos, también leerá el valor de la versión mientras lee los datos. Al enviar la actualización, solo se actualizará si el valor de la versión recién leído es igual al valor de la versión en la base de datos actual; de lo contrario, vuelva a intentarlo. operación de actualización hasta que la actualización sea exitosa.

El control de concurrencia optimista cree que la probabilidad de carrera de datos entre transacciones es relativamente pequeña, así que hágalo de la manera más directa posible y no bloquee hasta el momento del envío, por lo que no habrá bloqueos ni interbloqueos.

5. Realización concreta

5.1 Implementación del bloqueo pesimista

La implementación del bloqueo pesimista a menudo se basa en el mecanismo de bloqueo proporcionado por la base de datos. En la base de datos, el proceso de bloqueo pesimista es el siguiente:

  • 1. Antes de modificar el registro, intente agregar bloqueos exclusivos al registro.
  • 2. Si el bloqueo falla, lo que indica que el registro se está modificando, es posible que la consulta actual tenga que esperar o generar una excepción. El desarrollador determina el método de respuesta específico de acuerdo con las necesidades reales.
  • 3. Si el bloqueo es exitoso, el registro se puede modificar y la transacción se desbloqueará una vez que se complete la transacción.
  • 4. Durante el período, si hay otras operaciones que modifican o agregan bloqueos exclusivos al registro, esperarán el desbloqueo o lanzarán una excepción directamente.

Específicamente, tomamos el motor MySql Innodb como ejemplo para ilustrar la aplicación de bloqueos pesimistas en SQL
Para usar bloqueos pesimistas, el conjunto de atributos de confirmación automática autocommit=0 de la base de datos MySQL debe estar desactivado. Debido a que MySQL usa el modo de confirmación automática de forma predeterminada, es decir, cuando se realiza una operación de actualización, MySQL envía inmediatamente el resultado.
La implementación específica se logra a través de seleccionar ... para actualizar
inserte la descripción de la imagen aquí

"seleccionar... para actualizar bloqueará la tabla o bloqueará la fila"

5.2 Implementación del bloqueo optimista

El bloqueo optimista no necesita depender del mecanismo de bloqueo de la base de
datos.Hay dos pasos principales:

  • detección de colisiones
  • Actualización de datos.

Uno típico es CAS (Compare and Swap).
CAS significa comparar e intercambiar.

Es un mecanismo para resolver la pérdida de rendimiento causada por el uso de bloqueos en el caso de paralelismo de subprocesos múltiples.La operación CAS contiene tres operandos: ubicación de memoria (V), valor original esperado (A) y valor nuevo (B). Si el valor de la ubicación de la memoria (V) coincide con el valor original esperado (A), el procesador actualiza automáticamente el valor de la ubicación al nuevo valor (B). De lo contrario, el procesador no hace nada. En cualquier caso, devuelve el valor en esa ubicación antes de la instrucción CAS. CAS efectivamente dice: "Creo que la posición (V) debe contener el valor (A). Si es así, coloque el nuevo valor (B) en esta posición; de lo contrario, no cambie la posición, solo dígame cuál es la posición ahora valora". En Java, la clase sun.misc.Unsafe proporciona operaciones atómicas a nivel de hardware para implementar este CAS. Una gran cantidad de clases del paquete java.util.concurrent utilizan las operaciones CAS de esta clase Unsafe.java.

Cuando varios subprocesos intentan usar CAS para actualizar la misma variable al mismo tiempo, solo uno de los subprocesos puede actualizar el valor de la variable, mientras que los otros subprocesos fallan. El subproceso fallido no se suspenderá, pero se le indicará que falle. en esta competencia y puede volver a intentarlo. Por ejemplo, en el problema anterior de deducir inventario, podemos lograrlo mediante el bloqueo optimista.
inserte la descripción de la imagen aquí

Antes de actualizar, primero verifique la cantidad de inventario actual en la tabla de inventario y luego use la cantidad de inventario como una condición de modificación al realizar la actualización.
Al enviar la actualización, se considera que el número de inventario actual del registro correspondiente en la tabla de la base de datos se compara con el número de inventario extraído por primera vez. Si el número de inventario actual en la tabla de la base de datos es igual al número de inventario extraído fuera por primera vez, se actualizará, de lo contrario se considerarán datos caducados.
¿Crees que la operación anterior está bien?
De hecho, la operación anterior encontrará un problema ABA muy común:
inserte la descripción de la imagen aquí

  • El subproceso 1 recupera el número de inventario 3 de la base de datos, y el subproceso 2 también recupera el número de inventario 3 de la base de datos en este momento, y el subproceso 2 realiza algunas operaciones para convertirse en 2.
  • Luego, el subproceso 2 vuelve a cambiar el número de inventario a 3. En este momento, el subproceso 1 realiza una operación CAS y encuentra que la base de datos sigue siendo 3, y luego el subproceso 1 tiene éxito.
  • Aunque la operación CAS del subproceso uno es exitosa, no significa que el proceso esté libre de problemas.

Una mejor solución es pasar un campo de versión independiente incrementado secuencialmente. La optimización es la siguiente:
inserte la descripción de la imagen aquí

Cada vez que el bloqueo optimista realiza una operación de modificación de datos, traerá un número de versión. Una vez que el número de versión sea consistente con el número de versión de datos, se puede realizar la operación de modificación y el número de versión será +1. De lo contrario, la ejecución será fallar. Debido a que el número de versión aumenta con cada operación, no hay problemas de ABA. Además de la versión, las marcas de tiempo también se pueden usar porque las marcas de tiempo aumentan inherentemente de forma secuencial.
¿Pero sientes que esto está bien otra vez?
¡Estás equivocado otra vez!
Es decir, una vez que encuentres una alta concurrencia, solo un subproceso puede modificarse con éxito, entonces habrá muchas fallas. Para sitios web de comercio electrónico como Taobao, la alta concurrencia es común y obviamente no es razonable que los usuarios perciban fallas. Por lo tanto, todavía es necesario encontrar una forma de reducir la granularidad del bloqueo optimista. ¡Una mejor sugerencia es reducir la fuerza del bloqueo optimista, maximizar la tasa de rendimiento y mejorar la capacidad de concurrencia! como sigue:
inserte la descripción de la imagen aquí

En la instrucción SQL anterior, si el número de pedidos realizados por el usuario es 1, el control de bloqueo optimista se realiza mediante la cantidad - 1 > 0. Durante la ejecución, se consulta el valor de la cantidad y se resta 1 en una operación atómica.

El control de granularidad de bloqueo es un conocimiento importante en un entorno de alta concurrencia. Elegir un buen candado puede mejorar en gran medida la tasa de rendimiento y el rendimiento al tiempo que garantiza la seguridad de los datos.

6. Comprender el CAS subyacente

inserte la descripción de la imagen aquí

Si hay 3 subprocesos al mismo tiempo para modificar el valor de un AtomicInteger, el mecanismo subyacente es el siguiente:

  • Primero, cada subproceso obtiene el valor actual y luego realiza una operación CAS atómica.
    Atómico significa que esta operación CAS debe ejecutarse completamente por sí misma y no será interrumpida por otros.
  • Luego en la operación CAS, se comparará para ver si el valor actual es el valor recién obtenido. Si es así, significa que nadie ha cambiado este valor y luego establecerlo en un valor después de agregar 1.
  • De manera similar, si alguien encuentra que el valor obtenido anteriormente es diferente del valor actual al ejecutar CAS, hará que CAS falle. En caso de falla, ingrese un ciclo infinito, obtenga el valor nuevamente y luego realice la operación CAS.

7. Aplicación típica CAS - atómica

La mayoría de las clases del paquete java.util.concurrent.atomic se implementan mediante operaciones CAS, como AtomicInteger, AtomicBoolean y AtomicLong.
Generalmente, cuando la competencia no es muy feroz, el rendimiento de la operación atómica de usar este paquete es mucho más eficiente que usar la palabra clave sincronizada (ver getAndSet(), podemos ver que si la competencia de recursos es muy feroz, este bucle for puede durar durante mucho tiempo y no se puede usar durante mucho tiempo. Saltó con éxito. Sin embargo, en este caso, puede ser necesario considerar reducir la competencia de recursos).
Una aplicación muy típica es la aplicación de conteo

public class Increment {
    
    
    private int count = 0;
    public void add() {
    
    
        count++;
    }
}

¿A qué situación debemos enfrentarnos en el código anterior: no
es seguro incrementar el conteo en un entorno concurrente, por qué no es seguro y cómo resolver este problema?

En un entorno concurrente, ¿la operación de incremento automático de conteo no es segura? Porque count++ no es una operación atómica, sino una combinación de tres operaciones atómicas:

  • Lea el valor de conteo en la memoria y asígnelo a la variable local temp;
  • Ejecutar operación temp+1;
  • Asigne temp para contar.

Por lo tanto, si dos subprocesos ejecutan count++ al mismo tiempo, no hay garantía de que el subproceso 1 ejecute los tres pasos anteriores en secuencia antes de que el subproceso 2 comience a ejecutarse.

La solución al problema inseguro de count ++ en un entorno concurrente

bloqueo sincronizado

Solo un subproceso puede bloquearse al mismo tiempo, y otros subprocesos deben esperar el bloqueo, para que no ocurra el problema del recuento inexacto:

public class Increment {
    
    
    private int count = 0;
    public synchronized void add() {
    
    
        count++;
    }
}

Usted piensa que todo estará bien si resuelve la seguridad de subprocesos. No solo necesita que la función se complete normalmente, sino que también se asegura de que se pueda usar normalmente.
Sin embargo, la introducción de sincronizado causará el problema de la cola de subprocesos múltiples, lo que es equivalente a serializar cada subproceso, uno por uno. Poner en cola, bloquear, procesar datos, liberar bloqueos y pasar a continuación. Solo se ejecuta un subproceso al mismo tiempo, tal bloqueo es un poco "pesado".
Esto es similar a la implementación de bloqueos pesimistas.Si necesita adquirir este recurso, lo bloqueará y otros subprocesos no podrán acceder al recurso hasta que se libere el bloqueo del recurso después de la operación. Aunque se han hecho muchas optimizaciones para sincronizar con la actualización de la versión de Java, todavía es "demasiado pesado" para hacer frente a una operación de acumulación tan simple.

clase atómica atómica

Para la operación de count++, se puede hacer de otra manera, el paquete de concurrencia de Java proporciona una serie de clases atómicas atómicas, como AtomicInteger:

//import java.util.concurrent.atomic.AtomicInteger;
public static void main(String[] args) {
    
    
    public static AtomicInteger count = new AtomicInteger(0);
    public static void increase() {
    
    
        count.incrementAndGet();
    }
}

Múltiples subprocesos pueden ejecutar incrementAndGet() de AtomicInteger al mismo tiempo, lo que significa incrementar el valor de count en 1 y luego devolver el valor más reciente después de la acumulación. De hecho, la capa inferior de la clase atómica Atomic no es el mecanismo de bloqueo tradicional, sino el mecanismo CAS sin bloqueo, que garantiza la seguridad de subprocesos múltiples para modificar un valor a través del mecanismo CAS.

8. Optimización del rendimiento de CAS

Una gran cantidad de subprocesos modifican simultáneamente un AtomicInteger al mismo tiempo, y puede haber muchos subprocesos que giran continuamente y entran en un bucle que se repite infinitamente. Estos subprocesos siguen obteniendo el valor y luego inician la operación CAS, pero descubren que otros han cambiado el valor, por lo que ingresan al siguiente ciclo nuevamente, obtienen el valor, inician la operación CAS y fallan nuevamente, e ingresan al siguiente ciclo otra vez. Cuando una gran cantidad de subprocesos actualizan AtomicInteger con alta simultaneidad, este problema puede ser más obvio, lo que resulta en una gran cantidad de bucles vacíos de subprocesos, giro automático, rendimiento y eficiencia que no son particularmente buenos. Entonces, ¿cómo optimizarlo?
Java8 tiene una nueva clase, LongAdder, que intenta usar CAS segmentado y migración automática de segmentos para mejorar en gran medida el rendimiento de las operaciones CAS de alta simultaneidad y subprocesos múltiples. ¿Cómo optimiza esta clase el rendimiento? Como se muestra en la figura:
inserte la descripción de la imagen aquí

La idea central de LongAdder es la separación de puntos de acceso , que es similar a la idea de diseño de ConcurrentHashMap.
Es para separar el valor en una matriz, y cuando se accede a múltiples subprocesos, se cuenta por un número asignado a través del algoritmo hash. El resultado final es la suma y acumulación de estas matrices. De esta forma, se reduce la granularidad del bloqueo.

9. Cómo elegir el bloqueo pesimista o el bloqueo optimista

En la elección del bloqueo optimista y el bloqueo pesimista, observe principalmente la diferencia entre los dos y los escenarios aplicables.

  • Eficiencia de respuesta: si se requiere una velocidad de respuesta muy alta, se recomienda utilizar un esquema de bloqueo optimista, que se ejecuta correctamente si tiene éxito y falla si falla, y no necesita esperar a que otra concurrencia libere el bloqueo. El bloqueo optimista no bloquea realmente y es eficiente. Una vez que no se comprenda bien la granularidad del bloqueo, la probabilidad de que se produzca un error en la actualización será relativamente alta y se producirá fácilmente un error empresarial.
  • Frecuencia de conflicto: si la frecuencia de conflicto es muy alta, se recomienda utilizar el bloqueo pesimista para garantizar la tasa de éxito. La frecuencia de los conflictos es alta y la elección de un bloqueo optimista requerirá varios reintentos para tener éxito, y el costo es relativamente alto.
  • Costo de reintento: si el costo de reintento es alto, se recomienda utilizar el bloqueo pesimista. Los bloqueos pesimistas se basan en bloqueos de bases de datos y son ineficaces. La probabilidad de una falla de actualización es relativamente baja.
  • Bloqueo optimista Si alguien actualiza antes que usted, su actualización debe ser rechazada, lo que permite al usuario volver a operar. Los bloqueos pesimistas esperan a que se complete la actualización anterior. Esta es también la diferencia.

Supongo que te gusta

Origin blog.csdn.net/zhiyikeji/article/details/123562209
Recomendado
Clasificación