Úselo con precaución, este método de Mybatis-Plus puede provocar un punto muerto.

1 restauración de escena

1.1 Información de versión

MySQL版本:5.6.36-82.1-log 
Mybatis-Plus的starter版本:3.3.2
存储引擎:InnoDB

1.2 Fenómeno de punto muerto

El estudiante A utilizó el com.baomidou.mybatisplus proporcionado por Mybatis-Plus enel entorno de producción.extension.service.IService#saveOrUpdate( T, método com.baomidou.mybatisplus.core.conditions.Wrapper) (en lo sucesivo, método B ) , en el escenario concurrente, la base de datos informó el siguiente error


2 ¿Por qué se produce un punto muerto en el bloqueo de espacios?

Como se muestra en la figura anterior, la base de datos informa un punto muerto. Hay miles de escenarios de punto muerto. ¿Por qué se determina que el método B es un punto muerto causado por un bloqueo de brecha?

2.1 ¿Qué es un punto muerto?

Dos transacciones esperan el bloqueo de la otra, lo que provoca que la otra se bloquee, lo que resulta en un punto muerto.

2.2 ¿Qué es un bloqueo de espacio?

  • El bloqueo de espacios es un tipo de bloqueo de fila de MySQL. A diferencia del bloqueo de registros, el bloqueo de espacios bloquea un espacio.

  • Las reglas de bloqueo son las siguientes:

MySQL buscará hacia la izquierda el primer valor que sea menor que el valor del índice actual, y hacia la derecha el primer valor que sea mayor que el valor del índice actual (si no hay ninguno, será infinito positivo) y bloqueará este intervalo para evitar otras transacciones en este intervalo. Insertar datos.

2.3 ¿Por qué MySQL introduce bloqueos de espacios?

Combinados con el bloqueo de registro para formar el bloqueo de tecla Siguiente, trabajan juntos para evitar lecturas fantasmas bajo el nivel de aislamiento de lectura repetible.

2.4 Análisis de bloqueo de brechas

En teoría, un marco de código abierto ha sido pulido durante muchos años y los métodos proporcionados no deberían causar errores tan graves, pero la teoría es solo teórica, el hecho es que se produjo un punto muerto, por lo que iniciamos una ronda de investigación en profundidad. . Primero, comencemos con el código fuente de este método, el código fuente es el siguiente:

    default boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper) {
        return this.update(entity, updateWrapper) || this.saveOrUpdate(entity);
    }

A juzgar por el código fuente, este método no sigue la rutina. La lógica normal debería ser ejecutar la consulta primero, modificarla si existe y agregarla si no existe. Sin embargo, este método realiza la modificación inmediatamente. Nos preguntamos si MySQL agregó algún bloqueo durante las modificaciones que causaron el interbloqueo, por lo que encontramos el DBA y obtuvimos el último registro de interbloqueo, es decir, ejecutamos mostrar estado del motor innodb , encontramos dos datos clave:

*** (1) TRANSACTION:
...省略日志
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71C lock_mode X locks gap before rec insert intention waiting
  
*** (2) TRANSACTION:
...省略日志
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 347 n bits 80 index `PRIMARY` of table `database_name`.`table_name` trx id 71D lock_mode X locks gap before rec insert intention waiting

Una traducción simple significa que cuando la transacción uno adquiere el bloqueo de intención de inserción, debe esperar a que se libere el bloqueo de espacio (agregado por la transacción dos). Al mismo tiempo, cuando la transacción dos adquiere el bloqueo de intención de inserción, también espera el bloqueo de espacio que se liberará (se agregó la transacción uno), **(Este artículo no analiza los bloqueos agregados por MySQL durante la modificación e inserción. Agregamos bloqueos de espacio durante las modificaciones y obtenemos bloqueos de intención de inserción durante la inserción como condiciones conocidas) ** Luego Volvamos al método B. En un escenario concurrente, ¿es muy grande? La probabilidad es que la transacción uno y la transacción dos estén esperando el bloqueo de brecha de la otra, lo que resulta en un punto muerto.


Ahora que tenemos la teoría, verifiquemos este escenario con datos reales.

2.5 Verificar el punto muerto del bloqueo de espacios

  • Prepare la siguiente estructura de tabla (en lo sucesivo denominada uno de verificación)
create table t_gap_lock(
id int auto_increment primary key comment '主键ID',
name varchar(64) not null comment '名称',
age int not null comment '年龄'
) comment '间隙锁测试表';
  • Prepare los siguientes datos de la tabla.
mysql> select * from t_gap_lock;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 张三 |  18 |
|  5 | 李四 |  19 |
|  6 | 王五 |  20 |
|  9 | 赵六 |  21 |
| 12 | 孙七 |  22 |
+----+------+-----+
  • Iniciamos la transacción uno y ejecutamos la siguiente declaración,Tenga en cuenta que no hemos enviado la transacción en este momento
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
  • Al mismo tiempo, abrimos la transacción dos y ejecutamos la siguiente declaración,Tampoco enviamos la transacción para la transacción dos
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 7;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
  • A continuación ejecutamos la siguiente declaración en la transacción uno.
mysql> insert into t_gap_lock(id, name, age) value (7,'间隙锁7',27);  
  • Descubriremos que la transacción uno está bloqueada y luego ejecutamos la siguiente declaración para ver la transacción que está actualmente bloqueada.
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 749:0:360:3
lock_trx_id: 749
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_space: 0
  lock_page: 360
   lock_rec: 3
  lock_data: 5
*************************** 2. row ***************************
    lock_id: 74A:0:360:3
lock_trx_id: 74A
  lock_mode: X,GAP
  lock_type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_space: 0
  lock_page: 360
   lock_rec: 3
  lock_data: 5
2 rows in set (0.00 sec)

Según lock_type y lock_mode, podemos ver claramente que el tipo de bloqueo es bloqueo de fila y el modo de bloqueo es bloqueo de espacio.

  • Al mismo tiempo, ejecutamos la siguiente declaración en la transacción dos
insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);
  • Tan pronto como se ejecuta la declaración anterior, la base de datos informa inmediatamente un punto muerto y revierte la transacción 2 (*** WE ROLL BACK TRANSACTION (2) se puede ver en el registro de punto muerto)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 

En este punto, los estudiantes cuidadosos encontrarán que, eh, usted creó deliberadamente una brecha y permitió que dos transacciones insertaran datos en las brechas de la otra. Esto es demasiado deliberado. Este escenario básicamente no sucede en entornos de producción. Sí, ¿cómo puede ocurrir tal escenario? ¿Existe en un entorno de producción? Los datos anteriores son solo para permitir que todos vean intuitivamente el proceso de interbloqueo del bloqueo de brecha. A continuación, tengamos otro conjunto de datos, al que llamamos verificación dos.

  • Aún verificamos la estructura de la tabla y los datos, y realizamos dicha operación. Primero, iniciamos la transacción uno y realizamos las siguientes operaciones, pero la transacción aún no se envía.
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 
  • Al mismo tiempo, iniciamos la transacción dos y realizamos la misma operación que la transacción uno. Nos sorprenderá descubrir que también tiene éxito.
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0 
  • Entonces realizamos las siguientes operaciones en la transacción uno y nos sorprendió descubrir que la transacción uno estaba bloqueada.
insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);  
  • Mientras la transacción uno estaba bloqueada, ejecutamos la misma declaración en la transacción dos y descubrimos que la base de datos informó inmediatamente un punto muerto.
insert into t_gap_lock(id, name, age) value (4,'间隙锁4',24);    
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

La verificación 2 reproduce completamente el proceso de bloqueo en línea, es decir, la transacción 1 ejecuta primero la declaración de actualización, la transacción 2 también ejecuta la declaración de actualización al mismo tiempo y luego ejecuta la declaración de consulta de clave principal tan pronto como la transacción descubre que no. Se ha actualizado. Se descubre que, de hecho, no lo hay, por lo que se ejecuta la instrucción de inserción, pero la inserción primero debe adquirir el bloqueo de intención de inserción. Al adquirir el bloqueo de intención de inserción, se descubre que la brecha ha sido bloqueada por la transacción dos. , por lo que la transacción comienza a esperar a que la transacción dos libere el bloqueo de brecha. De la misma manera, la transacción 2 también realiza la operación anterior, lo que finalmente hace que la transacción 1 y la transacción 2 esperen entre sí para liberar el bloqueo de brecha, lo que finalmente conduce a un punto muerto.

La verificación 2 también ilustra un problema, es decir, el bloqueo del espacio no es mutuamente excluyente, es decir, después de que la transacción bloquea el espacio A, la transacción 2 aún puede bloquear el espacio A.

3 ¿Cómo solucionarlo?

3.1 Apague el bloqueo de espacio (no recomendado)

  • Reduzca el nivel de aislamiento, por ejemplo, a lectura comprometida.

  • Modifique directamente my.cnf, cambie el interruptor innodb_locks_unsafe_for_binlog a 1 y el valor predeterminado es 0 para habilitarlo.

PD: El método anterior solo es aplicable a escenarios comerciales actuales donde el problema de la lectura fantasma no es realmente preocupante.

3.2 Personalizar el método saveOrUpdate (recomendado)

RecomendaciónEscribe el tuyo propioUn método saveOrUpdate. Por supuesto, también puedes utilizar directamente el método saveOrUpdate proporcionado por Mybatis-Plus. Sin embargo, Según el código fuente, habrá muchas operaciones de reflexión adicionales y también se agregarán transacciones. Como todos sabemos, las operaciones de tabla única de MySQL no requieren ninguna transacción, lo que aumentará la sobrecarga adicional.

  @Transactional(
        rollbackFor = {Exception.class}
    )
    public boolean saveOrUpdate(T entity) {
        if (null == entity) {
            return false;
        } else {
            Class<?> cls = entity.getClass();
            TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
            Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!", new Object[0]);
            String keyProperty = tableInfo.getKeyProperty();
            Assert.notEmpty(keyProperty, "error: can not execute. because can not find column for id from entity!", new Object[0]);
            Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
            return !StringUtils.checkValNull(idVal) && !Objects.isNull(this.getById((Serializable)idVal)) ? this.updateById(entity) : this.save(entity);
        }
    }

4 Expandir

4.1 ¿Qué pasa si dos transacciones modifican una fila que existe?

En la verificación dos, las dos transacciones modificaron filas no existentes y ambas agregaron con éxito bloqueos de espacio. Si las dos transacciones modificaron filas existentes, ¿MySQL aún agregaría bloqueos de espacio? ¿O reducir el bloqueo del espacio de bloquear el espacio a bloquear una fila? Ante las dudas realizamos la siguiente verificación de datos, seguimos usando la tabla y los datos de la verificación 1.

  • Primero abrimos la transacción y ejecutamos la siguiente declaración
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
  • Abrimos la transacción dos nuevamente, ejecutamos la misma declaración y descubrimos que la transacción dos ha sido bloqueada.
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_gap_lock t set t.age = 25 where t.id = 1;
  • En este momento, ejecutamos la siguiente declaración para ver las transacciones que se están bloqueando actualmente.
mysql> SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS \G;
*************************** 1. row ***************************
    lock_id: 75C:0:360:2
lock_trx_id: 75C
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_space: 0
  lock_page: 360
   lock_rec: 2
  lock_data: 1
*************************** 2. row ***************************
    lock_id: 75B:0:360:2
lock_trx_id: 75B
  lock_mode: X
  lock_type: RECORD
 lock_table: `test`.`t_gap_lock`
 lock_index: `PRIMARY`
 lock_space: 0
  lock_page: 360
   lock_rec: 2
  lock_data: 1
2 rows in set (0.00 sec)

Según lock_type y lock_mode, vemos que el bloqueo agregado por las transacciones uno y dos se convierte en Record Lock, y no se agrega ningún bloqueo de espacio. Según los datos anteriores, se verifica que MySQL agregará Record Lock a la fila al modificar el existente datos, que es diferente del bloqueo de brecha, lo importante es que el bloqueo es mutuamente excluyente, es decir, diferentes transacciones no pueden agregar Record Lock a la misma fila de registros al mismo tiempo.

5. Conclusión

Aunque este método proporcionado por Mybatis-Plus puede causar un punto muerto, aún es innegable que es un excelente marco mejorado. El método de escritura lambda que proporciona mejora en gran medida nuestra eficiencia de desarrollo en el trabajo diario, por lo que todo es fluido. Para usar la dualidad, Debe adherirse a una actitud dialéctica, probar métodos familiares y utilizar métodos desconocidos con precaución.

Lo anterior es todo el proceso de nuestro análisis de bloqueo de brechas en el entorno de producción. Si cree que este artículo le ha dado un poco de comprensión sobre los bloqueos de brechas y los bloqueos de brechas, no olvide hacer clic tres veces con un solo clic y brindar soporte. Tecnología Zhuanzhuan La tecnología Zhuanzhuan traerá más práctica de producción y exploración a todos en el futuro.


Zhuanzhuan es una plataforma de intercambio y aprendizaje técnico para centros de I+D y socios de la industria, que comparte periódicamente experiencia práctica de primera línea y temas técnicos de vanguardia en la industria.
Siga las cuentas públicas "Zhuanzhuan Technology" (integral), "Zhuanzhuan FE" (centrada en FE), "Zhuanzhuan QA" (centrada en QA) y prácticas más útiles. Bienvenido a comunicarse y compartir~

Supongo que te gusta

Origin blog.csdn.net/zhuanzhuantech/article/details/134981055
Recomendado
Clasificación