A partir de 0, escritura a mano de transacciones MySQL

Dilo de frente: a partir de 0, el valor de aprendizaje de escribir a mano MySQL

Un tipo de 7 años de experiencia que una vez fue asesorado por Nien recibió un salario mensual de 40K en virtud de su dominio de Mysql.

A partir de 0, el valor de aprendizaje de escribir a mano un MySQL radica en:

  • Puede comprender profundamente el mecanismo interno y el principio de MySQL. Se puede decir que Mysql es el enfoque absoluto y la dificultad de la entrevista.
  • Para comprender mejor el uso y la optimización de MySQL.
  • Ayudarlo a mejorar sus habilidades de programación y habilidades para resolver problemas.
  • Como proyecto de rueda de currículum de alta calidad, tenga en cuenta que este es un proyecto de rueda de currículum de alta calidad

Los proyectos de muchos socios pequeños son muy bajos y hay una falta extrema de proyectos de ruedas, así que aquí viene el proyecto de ruedas.

Directorio de artículos

Diseño de arquitectura DB manuscrita:

El estilo de Nien: antes de comenzar a escribir código, primero haga la arquitectura

Funcionalmente, una arquitectura de sistema de base de datos escrita a mano se divide en los siguientes módulos:

  • Gestor de datos DM
  • Transaction ManagerTM
  • Administrador de versiones (VM)
  • Administrador de mesas (TBM)
  • Administrador de índices (MI),

Diagrama de diseño de diseño de arquitectura DB escrito a mano, de la siguiente manera:

A partir de 0, transacción Mysql de escritura a mano

Una transacción es una secuencia coherente de operaciones en una aplicación, todas las cuales deben completarse correctamente; de ​​lo contrario, se deshacen todos los cambios realizados durante cada operación.

Es decir, las transacciones son atómicas y una serie de operaciones en una transacción tienen éxito o no se realiza ninguna.

Hay dos formas de finalizar una transacción: cuando todos los pasos de la transacción se ejecutan con éxito, la transacción se confirma.

Si uno de estos pasos falla, se produce una operación de reversión, deshaciendo todas las operaciones al comienzo de la transacción.

El enunciado de la definición de transacción es el siguiente

(1) COMENZAR TRANSACCIÓN : La transacción comienza.

(2) FINALIZAR TRANSACCIÓN : La transacción finaliza.

(3) COMMIT : Confirmación de transacción. Esta operación indica el final exitoso de la transacción y notifica al administrador de transacciones que todas las operaciones de actualización de la transacción ahora se pueden confirmar o retener de forma permanente.

(4) ROLLBACK : Reversión de transacciones. Esta operación indica que la transacción finalizó sin éxito. Notificará al administrador de transacciones que ocurrió una falla, la base de datos puede estar en un estado inconsistente y todas las operaciones de actualización de la transacción deben revertirse o deshacerse.

5 estados de cosas

Una transacción es la unidad de ejecución básica de una base de datos, y si la transacción se ejecuta con éxito, la base de datos ingresa a otro estado consistente desde un estado consistente.

El estado del estado de la transacción tiene los siguientes 5 tipos

  • Estado activo : el estado inicial de la transacción, que se encuentra en este estado cuando se ejecuta la transacción.
  • Estado parcialmente comprometido : cuando se ejecuta la última instrucción de la secuencia de operaciones, la transacción se encuentra en el estado parcialmente comprometido. En este momento, aunque la transacción se haya ejecutado por completo, debido a que la salida real aún puede residir temporalmente en la memoria, es posible que ocurra una falla de hardware antes de que la transacción se complete con éxito, por lo tanto, el estado de confirmación parcial no significa que la transacción se haya ejecutado con éxito. .
  • Estado de falla : debido a errores de hardware o lógicos, la transacción no puede continuar ejecutándose normalmente, y la transacción ingresa al estado de falla, y la transacción en el estado de falla debe revertirse. De esta forma, la transacción entra en estado suspendido.
  • Estado cancelado : la transacción se revierte y la base de datos se restaura al estado en el que se encontraba antes de que comenzara la transacción.
  • Estado de confirmación : cuando la transacción se completa con éxito, se dice que la transacción está en estado de confirmación. Solo después de que la transacción esté en el estado comprometido, se puede decir que la transacción se ha comprometido.

Si por alguna razón la transacción no se ejecuta con éxito, pero ya ha modificado la base de datos, puede causar que la base de datos esté en un estado inconsistente en este momento, y es necesario deshacer (revertir) los cambios causados ​​por la transacción. .

Transiciones entre 5 estados de una transacción

  • COMENZAR TRANSACCIÓN : comienza a ejecutar una transacción, activa la transacción
  • END TRANSACTION : indica que todas las operaciones de lectura y escritura en la transacción se completaron, la transacción ingresa en un estado de compromiso parcial y el impacto de todas las operaciones de la transacción en la base de datos se almacena en la base de datos.
  • COMMIT : marca que la transacción se ha completado con éxito, el impacto de todas las operaciones en la transacción en la base de datos se ha almacenado de forma segura en la base de datos, la transacción ingresa al estado de confirmación y finaliza la operación de la transacción.
  • ABORT : marque la transacción para ingresar al estado de falla, el sistema cancela el impacto de todas las operaciones en la transacción en la base de datos y otras transacciones, y finaliza la operación de la transacción.

¿Cómo gestionar el estado de la transacción?

En la base de datos manuscrita MYDB, cada transacción tiene un XID, que identifica de forma única la transacción.

El XID de la transacción comienza desde 1 y se incrementa solo, y no se puede repetir.

El administrador de transacciones TM mantiene el estado de la transacción manteniendo el archivo XID y proporciona una interfaz para que otros módulos consulten el estado de una determinada transacción.

Se estipula que XID 0 es una súper transacción (Super Transaction).

Cuando desee realizar algunas operaciones sin solicitar una transacción, puede establecer el XID de la operación en 0. El estado de una transacción con un XID de 0 siempre se confirma.

Cada transacción tiene tres estados:

  • activo , en progreso, aún no terminado, es decir, el estado activo
  • comprometido , estado enviado
  • abortado , estado de reversión

Se define de la siguiente manera:

 // 事务的三种状态
//活动状态
private static final byte FIELD_TRAN_ACTIVE   = 0;
//已提交状态
private static final byte FIELD_TRAN_COMMITTED = 1;
//回滚状态
private static final byte FIELD_TRAN_ABORTED  = 2;

El archivo XID asigna un byte de espacio a cada transacción para almacenar su estado.

Al mismo tiempo, se almacena un número de 8 bytes en la cabecera del archivo XID, registrando el número de transacciones gestionadas por el archivo XID.

Por lo tanto, el estado de la transacción xid en el archivo se almacena en (xid-1)+8 bytes, y xid-1 se debe a que no es necesario registrar el estado de xid 0 (Super XID).

Algunas interfaces se proporcionan en TransactionManager para que otros módulos las llamen para crear transacciones y consultar el estado de las transacciones. Los métodos de interfaz son los siguientes:

//开启事务
long begin();
//提交事务
void commit(long xid);
//撤销事务
void abort(long xid);
// 判断事务状态-活动状态
boolean isActive(long xid);
// 是否提交状态
boolean isCommitted(long xid);
// 是否失败状态
boolean isAborted(long xid);
// 关闭事务管理TM
void close();

crear archivo xid

Necesitamos crear un archivo xid y crear un objeto TM, la implementación específica es la siguiente:

public static TransactionManagerImpl create(String path) {
    
    
	File f = new File(path+TransactionManagerImpl.XID_SUFFIX);
    try {
    
    
        if(!f.createNewFile()) {
    
    
            Panic.panic(Error.FileExistsException);
        }
    } catch (Exception e) {
    
    
        Panic.panic(e);
    }
    if(!f.canRead() || !f.canWrite()) {
    
    
        Panic.panic(Error.FileCannotRWException);
    }

    FileChannel fc = null;
    RandomAccessFile raf = null;
    try {
    
    
        raf = new RandomAccessFile(f, "rw");
        fc = raf.getChannel();
    } catch (FileNotFoundException e) {
    
    
       Panic.panic(e);
    }

    // 写空XID文件头
    ByteBuffer buf = ByteBuffer.wrap(new byte[TransactionManagerImpl.LEN_XID_HEADER_LENGTH]);
    try {
    
    
        //从零创建 XID 文件时需要写一个空的 XID 文件头,即设置 xidCounter 为 0,
        // 否则后续在校验时会不合法:
        fc.position(0);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }

    return new TransactionManagerImpl(raf, fc);
}

//从一个已有的 xid 文件来创建 TM
public static TransactionManagerImpl open(String path) {
    
    
    File f = new File(path+TransactionManagerImpl.XID_SUFFIX);
    if(!f.exists()) {
    
    
        Panic.panic(Error.FileNotExistsException);
    }
    if(!f.canRead() || !f.canWrite()) {
    
    
        Panic.panic(Error.FileCannotRWException);
    }

    FileChannel fc = null;
    RandomAccessFile raf = null;
    try {
    
    
        //用来访问那些保存数据记录的文件
        raf = new RandomAccessFile(f, "rw");
        //返回与这个文件有关的唯一FileChannel对象
        fc = raf.getChannel();
    } catch (FileNotFoundException e) {
    
    
       Panic.panic(e);
    }

    return new TransactionManagerImpl(raf, fc);
}

definir constante

Veamos la clase de implementación TransactionManagerImpl de la interfaz TransactionManager Primero defina algunas constantes necesarias

// XID文件头长度
static final int LEN_XID_HEADER_LENGTH = 8;
// 每个事务的占用长度
private static final int XID_FIELD_SIZE = 1;

// 事务的三种状态
//活动状态
private static final byte FIELD_TRAN_ACTIVE   = 0;
//已提交状态
private static final byte FIELD_TRAN_COMMITTED = 1;
//回滚状态
private static final byte FIELD_TRAN_ABORTED  = 2;

// 超级事务,永远为commited状态
public static final long SUPER_XID =1;

// XID 文件后缀
static final String XID_SUFFIX = ".xid";

private RandomAccessFile file;
private FileChannel fc;
private long xidCounter;
//显示锁
private Lock counterLock;

FileChannel se usa para leer y escribir archivos en modo NIO. FileChannel proporciona una forma de acceder a archivos a través de canales. Puede usar el método position(int) con parámetros para ubicar cualquier posición en el archivo y comenzar a operar. También puede asignar archivos a memoria directa para mejorar la eficiencia de acceso a archivos de gran tamaño. Acerca de java NIO, consulte <<Programación central de alta concurrencia de java (Volumen 1)>>.

Verifique que el archivo XID sea legal

Después de que el constructor crea un TransactionManager, primero debe verificar el archivo XID para asegurarse de que sea un archivo XID legal.

El método de verificación también es muy simple: la longitud teórica del archivo se deduce del número de 8 bytes en el encabezado del archivo y se compara con la longitud real del archivo. Si es diferente, el archivo XID se considera no válido.

TransactionManagerImpl(RandomAccessFile raf, FileChannel fc) {
    
    
    this.file = raf;
    this.fc = fc;
    //显式锁
    counterLock = new ReentrantLock();
    checkXIDCounter();
}

/**
 * 检查XID文件是否合法
 * 读取XID_FILE_HEADER中的xidcounter,根据它计算文件的理论长度,对比实际长度
 */
private void checkXIDCounter() {
    
    
    long fileLen = 0;
    try {
    
    
        fileLen = file.length();
    } catch (IOException e1) {
    
    
        Panic.panic(Error.BadXIDFileException);
    }
    if(fileLen < LEN_XID_HEADER_LENGTH) {
    
    
        //对于校验没有通过的,会直接通过 panic 方法,强制停机。
        // 在一些基础模块中出现错误都会如此处理,
        // 无法恢复的错误只能直接停机。
        Panic.panic(Error.BadXIDFileException);
    }

    // java NIO中的Buffer的array()方法在能够读和写之前,必须有一个缓冲区,
    // 用静态方法 allocate() 来分配缓冲区
    ByteBuffer buf = ByteBuffer.allocate(LEN_XID_HEADER_LENGTH);
    try {
    
    
        fc.position(0);
        fc.read(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    //从文件开头8个字节得到事务的个数
    this.xidCounter = Parser.parseLong(buf.array());
    // 根据事务xid取得其在xid文件中对应的位置
    long end = getXidPosition(this.xidCounter + 1);
    if(end != fileLen) {
    
    
        //对于校验没有通过的,会直接通过 panic 方法,强制停机
        Panic.panic(Error.BadXIDFileException);
    }
}

El bloqueo utiliza el bloqueo reentrante ReentrantLock. ReentrantLock es una clase de implementación básica del bloqueo explícito proporcionado por el paquete JUC. La clase ReentrantLock implementa la interfaz Lock. Tiene la misma simultaneidad y semántica de memoria que la sincronización, pero tiene prioridad limitada en el tiempo. , Algunas funciones de bloqueo avanzadas, como la prioridad interrumpible. Para conocer el contenido de ReenttrantLock, consulte <<Programación central de alta concurrencia de Java (Volumen 2)>>.

Longitud teórica del archivo: los primeros 8 bytes + los bytes ocupados por un estado de transacción * el número de transacciones;

Utilice los 8 bytes al principio del archivo xid (registrar el número de transacciones) para deducir la longitud teórica del archivo y compararla con la longitud real del archivo. De lo contrario, el archivo XID se considera no válido. Se comprobará cada vez que se cree el objeto xid.

Para aquellos que no pasen la verificación, se utilizará directamente el método de pánico para forzar el apagado. Los errores en algunos módulos básicos se manejarán de esta manera, y los errores que no se pueden recuperar solo se pueden detener directamente.

Para obtener el desplazamiento del estado xid en el archivo, el método getXidPosition() se implementa de la siguiente manera:

// 根据事务xid取得其在xid文件中对应的位置
private long getXidPosition(long xid) {
    
    
    return LEN_XID_HEADER_LENGTH + (xid-1)*XID_FIELD_SIZE;
}

transacción abierta

begin()Abra una transacción, inicialice la estructura de la transacción y guárdela en activeTransaction para inspección y uso de instantáneas:

 /**
 *begin() 每开启一个事务,并计算当前活跃的事务的结构,将其存放在 activeTransaction 中,
 * 用于检查和快照使用:
 * @param level
 * @return
 */
@Override
public long begin(int level) {
    
    
    lock.lock();
    try {
    
    
        long xid = tm.begin();
        //activeTransaction 当前事务创建时活跃的事务,,如果level!=0,放入t的快照中
        Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
        activeTransaction.put(xid, t);
        return xid;
    } finally {
    
    
        lock.unlock();
    }
}

cambiar el estado de la transacción

El desplazamiento de la transacción xid = los primeros 8 bytes + los bytes ocupados por un estado de transacción * el xid de la transacción;

Calcule el desplazamiento para registrar el estado de la transacción a través de la transacción xid y luego cambie el estado.

La implementación específica es la siguiente:

// 更新xid事务的状态为status
private void updateXID(long xid, byte status) {
    
    
    long offset = getXidPosition(xid);
    //每个事务占用长度
    byte[] tmp = new byte[XID_FIELD_SIZE];
    tmp[0] = status;
    ByteBuffer buf = ByteBuffer.wrap(tmp);
    try {
    
    
        fc.position(offset);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    try {
    
    
        //将数据刷出到磁盘,但不包括元数据
        fc.force(false);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

Entre ellos, abort() y commit() llaman a este método,

// 提交XID事务
public void commit(long xid) {
    
    
    updateXID(xid, FIELD_TRAN_COMMITTED);
}

// 回滚XID事务
public void abort(long xid) {
    
    
    updateXID(xid, FIELD_TRAN_ABORTED);
}

Actualizar encabezado xid

Cada vez que se crea una transacción, la cantidad de transacciones registradas en el encabezado del archivo debe ser +1;

// 将XID加一,并更新XID Header
private void incrXIDCounter() {
    
    
    xidCounter ++;
    ByteBuffer buf = ByteBuffer.wrap(Parser.long2Byte(xidCounter));
    //游标pos, 限制为lim, 容量为cap
    try {
    
    
        fc.position(0);
        fc.write(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    try {
    
    
        fc.force(false);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

Juzgar el estado de la transacción

Según xid-"obtener el desplazamiento del estado de la transacción de registro xid-"leer el estado de la transacción xid-"si es igual al estado.

// 检测XID事务是否处于status状态
private boolean checkXID(long xid, byte status) {
    
    
    long offset = getXidPosition(xid);
    ByteBuffer buf = ByteBuffer.wrap(new byte[XID_FIELD_SIZE]);
    try {
    
    
        fc.position(offset);
        fc.read(buf);
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
    return buf.array()[0] == status;
}

// 活动状态判断
public boolean isActive(long xid) {
    
    
    if(xid == SUPER_XID) return false;
    return checkXID(xid, FIELD_TRAN_ACTIVE);
}

// 已提交状态判断
public boolean isCommitted(long xid) {
    
    
    if(xid == SUPER_XID) return true;
    return checkXID(xid, FIELD_TRAN_COMMITTED);
}

//回滚状态判断
public boolean isAborted(long xid) {
    
    
    if(xid == SUPER_XID) return false;
    return checkXID(xid, FIELD_TRAN_ABORTED);
}

Cerrar MT

 //TM关闭
public void close() {
    
    
    try {
    
    
        fc.close();
        file.close();
    } catch (IOException e) {
    
    
        Panic.panic(e);
    }
}

Los bloqueos de dos fases implementan operaciones de transacción

Introducción a la función de bloqueo de dos fases (2PL)

La programación de transacciones generalmente incluye la programación en serie y la programación en paralelo; primero, comprendamos los siguientes conceptos:

  • Control de concurrencia : en un sistema compartido por varios usuarios, muchos usuarios pueden operar con los mismos datos al mismo tiempo.
  • Programación : el orden de ejecución de las transacciones
  • Programación en serie : las transacciones múltiples se ejecutan en serie en secuencia, y todas las operaciones de otra transacción se ejecutan solo después de que se ejecutan todas las operaciones de una transacción. Siempre que se trate de programación en serie, el resultado de la ejecución es correcto.

  • Programación paralela: use el método de tiempo compartido para procesar múltiples transacciones al mismo tiempo. Sin embargo, los resultados de programación de la programación paralela pueden ser incorrectos y pueden producir estados inconsistentes, que incluyen: modificación perdida, lectura no repetible y lectura de datos sucios.

Programación paralela de transacciones

En una transacción, se divide en una fase de bloqueo (lock) y una fase de desbloqueo (unlock), es decir, todas las operaciones de bloqueo son anteriores a la operación de desbloqueo, como se muestra en la siguiente figura:

Dos fases de bloqueo y desbloqueo de transacciones

En situaciones reales, SQL cambia constantemente y el número de entradas es incierto.Es difícil para la base de datos determinar cuál es la fase de bloqueo y cuál es la fase de desbloqueo en la transacción. Así que se introdujo S2PL (Strict-2PL), es decir, en la transacción, solo cuando se compromete o retrocede es la fase de desbloqueo, y el resto del tiempo es la fase de bloqueo. La introducción de 2PL es para garantizar el aislamiento de las transacciones, es decir, las transacciones múltiples son equivalentes a la ejecución en serie en el caso de concurrencia.

fase de bloqueo de bloqueo de 2 fases

La primera etapa es la etapa de obtención del bloqueo, que se denomina etapa de expansión: de hecho, esta etapa puede ingresar a la operación de bloqueo.Antes de leer cualquier dato, debe solicitar un bloqueo S, y antes de realizar una operación de escritura, debe debe solicitar y obtener un bloqueo X. , el bloqueo no tiene éxito, la transacción entra en el estado de espera y no continúa hasta que el bloqueo sea exitoso. Una vez bloqueado, no se puede desbloquear.

La segunda etapa es la etapa de liberación del bloqueo, que se denomina etapa de contracción: cuando la transacción libera un bloque, la transacción ingresa a la etapa de bloqueo, en la que solo se puede realizar el desbloqueo pero no se puede realizar la operación de bloqueo.

Implementación de transacciones de bloqueo en dos fases (2PL)

Las transacciones en mysql son transacciones implícitas por defecto. Al realizar operaciones de inserción, actualización y eliminación, la base de datos inicia, confirma o revierte automáticamente las transacciones.

La variable autocommit controla si habilitar transacciones implícitas.

Así que las transacciones se dividen en transacciones implícitas y transacciones explícitas .

Las transacciones implícitas se abren, confirman o revierten automáticamente mediante transacciones, como declaraciones de inserción, actualización y eliminación. La apertura, confirmación o reversión de transacciones se controla automáticamente mediante mysql.

Las transacciones explícitas deben abrirse, confirmarse o retrotraerse manualmente, y son controladas por el propio desarrollador.

Transacción manuscrita de bloqueo de dos fases (2PL)

Antes de implementar el nivel de aislamiento de transacciones, debemos analizar el Administrador de versiones (VM).

VM se basa en el protocolo de bloqueo de dos etapas para realizar la serialización de la secuencia de programación e implementa MVCC para eliminar el bloqueo de lectura y escritura.

Se implementan dos niveles de aislamiento al mismo tiempo.VM es el núcleo de gestión de transacciones y versiones de datos de la base de datos manuscrita MYDB.

almacenamiento de transacciones

Para un registro, MYDB usa la clase Entry para mantener su estructura.

Aunque en teoría, MVCC implementa varias versiones, pero en la implementación, VM no proporciona la operación de actualización, y la operación de actualización para los campos se implementa mediante la siguiente administración de tablas y campos (TBM).

Entonces, en la implementación de VM, solo hay una versión de un registro.

Un registro se almacena en un elemento de datos, así que simplemente guarde una referencia de elemento de datos en la entrada:

public class Entry {
    
    

    private static final int OF_XMIN = 0;
    private static final int OF_XMAX = OF_XMIN+8;
    private static final int OF_DATA = OF_XMAX+8;

    private long uid;
    private DataItem dataItem;
    private VersionManager vm;

    public static Entry newEntry(VersionManager vm, DataItem dataItem, long uid) {
    
    
        Entry entry = new Entry();
        entry.uid = uid;
        entry.dataItem = dataItem;
        entry.vm = vm;
        return entry;
    }

    public static Entry loadEntry(VersionManager vm, long uid) throws Exception {
    
    
        DataItem di = ((VersionManagerImpl)vm).dm.read(uid);
        return newEntry(vm, di, uid);
    }

    public void release() {
    
    
        ((VersionManagerImpl)vm).releaseEntry(this);
    }

    public void remove() {
    
    
        dataItem.release();
    }
}

El formato de los datos almacenados en una Entrada se estipula de la siguiente manera:

[XMIN]  [XMAX]  [DATA]
8个字节  8个字节

XMIN es el número de transacción que creó el registro (versión) y XMAX es el número de transacción que eliminó el registro (versión). DATOS son los datos contenidos en este registro.

De acuerdo con esta estructura, el método wrapEntryRaw() llamado al crear un registro es el siguiente:

public static byte[] wrapEntryRaw(long xid, byte[] data) {
    
    
    byte[] xmin = Parser.long2Byte(xid);
    byte[] xmax = new byte[8];
    return Bytes.concat(xmin, xmax, data);
}

De manera similar, si desea obtener los datos contenidos en el registro, debe analizarlos de acuerdo con esta estructura:

// 以拷贝的形式返回内容
public byte[] data() {
    
    
    dataItem.rLock();
    try {
    
    
        SubArray sa = dataItem.data();
        byte[] data = new byte[sa.end - sa.start - OF_DATA];
        System.arraycopy(sa.raw, sa.start+OF_DATA, data, 0, data.length);
        return data;
    } finally {
    
    
        dataItem.rUnLock();
    }
}

Aquí los datos se devuelven en forma de copia. Si necesita modificarlos, debe seguir un proceso determinado: debe llamar before()al método unBefore()cuando desee deshacer la modificación y llamar after()el método .

Todo el proceso es principalmente para guardar los datos de la fase anterior e iniciar sesión a tiempo. DM garantizará que la modificación de DataItem sea atómica.

@Override
public void before() {
    
    
    wLock.lock();
    pg.setDirty(true);
    System.arraycopy(raw.raw, raw.start, oldRaw, 0, oldRaw.length);
}

@Override
public void unBefore() {
    
    
    System.arraycopy(oldRaw, 0, raw.raw, raw.start, oldRaw.length);
    wLock.unlock();
}

@Override
public void after(long xid) {
    
    
    dm.logDataItem(xid, this);
    wLock.unlock();
}

Establecer el valor de XMAX refleja que si es necesario modificarlo, se deben seguir ciertas reglas, y esta versión es invisible para cada transacción después de XMAX, lo que equivale a la eliminación. El código de setXmax es el siguiente:

public void setXmax(long xid) {
    
    
    dataItem.before();
    try {
    
    
        SubArray sa = dataItem.data();
        System.arraycopy(Parser.long2Byte(xid), 0, sa.raw, sa.start+OF_XMAX, 8);
    } finally {
    
    
        dataItem.after(xid);
    }
}

transacción abierta

begin() calcula la estructura de la transacción activa actual cada vez que inicia una transacción y la almacena en activeTransaction,

@Override
public long begin(int level) {
    
    
    lock.lock();
    try {
    
    
        long xid = tm.begin();
        Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
        activeTransaction.put(xid, t);
        return xid;
    } finally {
    
    
        lock.unlock();
    }
}

confirmar transacción

El método commit() confirma una transacción, principalmente para liberar la estructura relacionada, liberar el bloqueo retenido, modificar el estado de TM y eliminar la transacción de activeTransaction.

@Override
public void commit(long xid) throws Exception {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    try {
    
    
        if(t.err != null) {
    
    
            throw t.err;
        }
    } catch(NullPointerException n) {
    
    
        System.out.println(xid);
        System.out.println(activeTransaction.keySet());
        Panic.panic(n);
    }

    lock.lock();
    activeTransaction.remove(xid);
    lock.unlock();

    lt.remove(xid);
    tm.commit(xid);
}

transacción de reversión

Hay dos formas de abortar una transacción, manual y automática.

Manual se refiere a llamar al método abort(), mientras que automático significa que cuando se detecta un interbloqueo en la transacción, la transacción de reversión se deshará automáticamente; o cuando se produce un salto de versión, también se revertirá automáticamente:

/**
 * 回滚事务
 * @param xid
 */
@Override
public void abort(long xid) {
    
    
    internAbort(xid, false);
}

private void internAbort(long xid, boolean autoAborted) {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
   //手动回滚
    if(!autoAborted) {
    
    
        activeTransaction.remove(xid);
    }
    lock.unlock();

    //自动回滚
    if(t.autoAborted) return;
    lt.remove(xid);
    tm.abort(xid);
}

eliminar transacción

Cuando una transacción se confirma o aborta, libera todos los bloqueos que tiene y se elimina del gráfico de espera.

Y elija un xid de la cola de espera para ocupar el uid.

Al desbloquear, simplemente desbloquee el objeto de bloqueo, para que otros subprocesos comerciales puedan obtener el bloqueo y continuar ejecutándose.

public void remove(long xid) {
    
    
    lock.lock();
    try {
    
    
        List<Long> l = x2u.get(xid);
        if(l != null) {
    
    
            while(l.size() > 0) {
    
    
                Long uid = l.remove(0);
                selectNewXID(uid);
            }
        }
        waitU.remove(xid);
        x2u.remove(xid);
        waitLock.remove(xid);

    } finally {
    
    
        lock.unlock();
    }
}

// 从等待队列中选择一个xid来占用uid
private void selectNewXID(long uid) {
    
    
    u2x.remove(uid);
    List<Long> l = wait.get(uid);
    if(l == null) return;
    assert l.size() > 0;

    while(l.size() > 0) {
    
    
        long xid = l.remove(0);
        if(!waitLock.containsKey(xid)) {
    
    
            continue;
        } else {
    
    
            u2x.put(uid, xid);
            Lock lo = waitLock.remove(xid);
            waitU.remove(xid);
            lo.unlock();
            break;
        }
    }

    if(l.size() == 0) wait.remove(uid);
}

insertar datos

insert() es envolver los datos en una entrada y entregarlos a DM para su inserción:

@Override
public long insert(long xid, byte[] data) throws Exception {
    
    
    lock.lock();
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    if(t.err != null) {
    
    
        throw t.err;
    }

    byte[] raw = Entry.wrapEntryRaw(xid, data);
    return dm.insert(xid, raw);
}

leer transacción

El método read() lee una entrada y la visibilidad se puede juzgar según el nivel de aislamiento.

@Override
public byte[] read(long xid, long uid) throws Exception {
    
    
    lock.lock();
    //当前事务xid读取时的快照数据
    Transaction t = activeTransaction.get(xid);
    lock.unlock();

    if(t.err != null) {
    
    
        throw t.err;
    }

    Entry entry = null;
    try {
    
    
        //通过uid找要读取的事务dataItem
        entry = super.get(uid);
    } catch(Exception e) {
    
    
        if(e == Error.NullEntryException) {
    
    
            return null;
        } else {
    
    
            throw e;
        }
    }
    try {
    
    
        if(Visibility.isVisible(tm, t, entry)) {
    
    
            return entry.data();
        } else {
    
    
            return null;
        }
    } finally {
    
    
        entry.release();
    }
}

Propiedades ACID de las transacciones

Una transacción es una unidad de ejecución de programa que accede y actualiza varios elementos de datos en una base de datos. El propósito de la transacción es modificar todo o no hacerlo.

En la mayoría de los escenarios, la aplicación solo necesita operar una sola base de datos, y la transacción en este caso se denomina transacción local (Transacción local).

La base de datos admite directamente las propiedades ACID de las transacciones locales.

Para lograr transacciones locales, Mysql ha trabajado mucho, como registros de reversión, registros de rehacer, MVCC, bloqueos de lectura y escritura, etc.

Para el motor de almacenamiento InnoDB, su nivel de aislamiento de transacciones predeterminado es Lectura repetible, que sigue y satisface completamente las características ACID de las transacciones.

ACID es un acrónimo de cuatro palabras: Atomicidad, Consistencia, Aislamiento y Durabilidad.

InnoDB garantiza las características ACID de las transacciones a través de registros y bloqueos:

  • A través del mecanismo de bloqueo de la base de datos, se garantiza el aislamiento de las transacciones;
  • Asegurar el aislamiento de las transacciones a través de Redo Log (redo log);
  • La atomicidad y consistencia de las transacciones están garantizadas a través de Undo Log (undo log);

Atomicidad

Una transacción debe ser una unidad de secuencia de operación atómica. Todas las operaciones contenidas en una transacción son exitosas o no se ejecutan al mismo tiempo. Si alguna falla, la transacción completa se retrotrae. Solo cuando todas las operaciones se ejecutan con éxito, la transacción completa considerarse un éxito.

Antes de operar cualquier dato, primero haga una copia de seguridad de los datos en Undo Log y luego modifique los datos. Si ocurre un error o el usuario ejecuta una declaración de reversión, el sistema puede usar la copia de seguridad en el registro de deshacer para restaurar los datos al estado anterior al inicio de la transacción, a fin de garantizar la atomicidad de los datos.

Consistencia

La ejecución de la transacción no puede destruir la integridad y consistencia de los datos de la base de datos, y la base de datos debe estar en un estado consistente antes y después de que se ejecute la transacción.

La coherencia incluye dos aspectos, a saber, la coherencia de las restricciones y la coherencia de los datos;

  • Consistencia de Restricciones: Restricciones tales como clave foránea, verificación (no admitida por mysql), índice único, etc. especificadas al crear la estructura de la tabla.
  • Consistencia de los datos: es una regulación integral, porque es el resultado de la atomicidad, la persistencia y el aislamiento, en lugar de depender únicamente de una determinada tecnología.

aislamiento

En una situación concurrente, las transacciones concurrentes están aisladas entre sí y la ejecución de una transacción no puede ser interferida por otras transacciones.

Es decir, cuando diferentes transacciones operan en los mismos datos simultáneamente, cada transacción tiene su propio espacio de datos completo, es decir, las operaciones y los datos utilizados dentro de una transacción están aislados de otras transacciones simultáneas, y las transacciones ejecutadas simultáneamente no pueden interferir entre sí. .

Durabilidad

La persistencia, también conocida como permanencia, significa que una vez que se confirma una transacción, sus cambios de estado en los datos correspondientes en la base de datos deben ser permanentes. Incluso si el sistema falla o la máquina deja de funcionar, siempre que la base de datos pueda reiniciarse, podrá restaurarla al estado en el que la transacción finalizó con éxito.

El registro de rehacer registra la copia de seguridad de los datos nuevos. Antes de que se confirme la transacción, solo necesita conservar el registro de rehacer y no es necesario conservar los datos. Cuando el sistema falla, aunque los datos no se conservan, el registro de rehacer ya ha sido persistente. El sistema puede restaurar todos los datos al estado anterior al bloqueo de acuerdo con el contenido del registro de rehacer.Este es el proceso de usar el registro de rehacer para garantizar la persistencia de los datos.

Resumir

La integridad de los datos refleja principalmente las características de consistencia. La integridad de los datos está garantizada por atomicidad, aislamiento y persistencia. Estas tres características están garantizadas por redo Log y Undo Log. La relación entre las propiedades ACID se muestra en la siguiente figura:

punto muerto

En mysql, el protocolo de bloqueo de dos fases (2PL) generalmente incluye dos fases de expansión y contracción. Durante la fase de expansión, la transacción adquirirá bloqueos, pero no podrá liberarlos. Durante la fase de contracción, se pueden liberar las esclusas existentes pero no se pueden adquirir nuevas esclusas, esta disposición tiene el riesgo de interbloqueo.

Por ejemplo: cuando T1' está en la etapa de expansión, adquiere el bloqueo de lectura de Y y lee Y. En este momento, quiere adquirir el bloqueo de escritura de X, pero encuentra que el bloqueo de lectura de T2' ha bloqueado X, y T2' también quiere adquirir un bloqueo de escritura en Y. En resumen, T1' no liberará Y a menos que obtenga X, y T2' no liberará X a menos que obtenga Y, por lo que queda atrapado en un bucle y se forma un interbloqueo.

2PL bloquea la transacción hasta que el subproceso que mantiene el bloqueo lo libera. Esta relación de espera se puede abstraer en un borde dirigido.Por ejemplo, si Tj está esperando a Ti, se puede expresar como Tj --> Ti. De esta forma, un número infinito de aristas dirigidas pueden formar un gráfico. Para detectar interbloqueos, solo necesita verificar si hay un ciclo en este gráfico.

detección de punto muerto

Cree un objeto LockTable y mantenga este gráfico en memoria.La estructura de mantenimiento es la siguiente:

public class LockTable {
    
    
    
    private Map<Long, List<Long>> x2u;  // 某个XID已经获得的资源的UID列表
    private Map<Long, Long> u2x;        // UID被某个XID持有
    private Map<Long, List<Long>> wait; // 正在等待UID的XID列表
    private Map<Long, Lock> waitLock;   // 正在等待资源的XID的锁
    private Map<Long, Long> waitU;      // XID正在等待的UID
    ......
}

Cada vez que haya una situación de espera, intente agregar un borde al gráfico y realice una detección de punto muerto. Si se detecta un interbloqueo, se retira el borde, no se permite la adición y se retira la transacción.

// 不需要等待则返回null,否则返回锁对象
// 会造成死锁则抛出异常
public Lock add(long xid, long uid) throws Exception {
    
    
    lock.lock();
    try {
    
    
        //某个xid已经获得的资源的UID列表,如果在这个列表里面,则不造成死锁,也不需要等待
        if(isInList(x2u, xid, uid)) {
    
    
            return null;
        }
        //表示有了一个新的uid,则把uid加入到u2x和x2u里面,不死锁,不等待
        // u2x  uid被某个xid占有
        if(!u2x.containsKey(uid)) {
    
    
            u2x.put(uid, xid);
            putIntoList(x2u, xid, uid);
            return null;
        }
        //以下就是需要等待的情况
        //多个事务等待一个uid的释放
        waitU.put(xid, uid);
        //putIntoList(wait, xid, uid);
        putIntoList(wait, uid, xid);
        //造成死锁
        if(hasDeadLock()) {
    
    
            //从等待列表里面删除
            waitU.remove(xid);
            removeFromList(wait, uid, xid);
            throw Error.DeadlockException;
        }
        //从等待列表里面删除
        Lock l = new ReentrantLock();
        l.lock();
        waitLock.put(xid, l);
        return l;

    } finally {
    
    
        lock.unlock();
    }
}

Llame a agregar, si no necesita esperar, vaya al siguiente paso, si necesita esperar, devolverá un objeto Bloquear bloqueado. Cuando la persona que llama adquiere el objeto, debe intentar adquirir el bloqueo del objeto, para lograr el propósito de bloquear el hilo.

 try {
    
    
    l = lt.add(xid, uid);
} catch(Exception e) {
    
    
    t.err = Error.ConcurrentUpdateException;
    internAbort(xid, true);
    t.autoAborted = true;
    throw t.err;
}
if(l != null) {
    
    
    l.lock();
    l.unlock();
}

punto muerto del juicio

El algoritmo para encontrar si hay un ciclo en un gráfico es en realidad una búsqueda profunda. Cabe señalar que este gráfico no es necesariamente un gráfico conexo. La idea específica es establecer un sello de acceso para cada nodo, que se inicializa en 1, y luego atravesar todos los nodos, usar cada nodo que no sea 1 como raíz para la búsqueda profunda y buscar todos los nodos encontrados en el gráfico conectado. el mismo número, diferentes gráficos conectados tienen números diferentes. De esta forma, si al recorrer un gráfico determinado te encuentras con un nodo previamente recorrido, significa que ha aparecido un ciclo.

La implementación específica de juzgar punto muerto es la siguiente:

private boolean hasDeadLock() {
    
    
    xidStamp = new HashMap<>();
    stamp = 1;
    System.out.println("xid已经持有哪些uid x2u="+x2u);//xid已经持有哪些uid
    System.out.println("uid正在被哪个xid占用 u2x="+u2x);//uid正在被哪个xid占用

    //已经拿到锁的xid
    for(long xid : x2u.keySet()) {
    
    
        Integer s = xidStamp.get(xid);
        if(s != null && s > 0) {
    
    
            continue;
        }
        stamp ++;
        System.out.println("xid"+xid+"的stamp是"+s);
        System.out.println("进入深搜");
        if(dfs(xid)) {
    
    
            return true;
        }
    }
    return false;
}

private boolean dfs(long xid) {
    
    
    Integer stp = xidStamp.get(xid);
    //遍历某个图时,遇到了之前遍历过的节点,说明出现了环
    if(stp != null && stp == stamp) {
    
    
        return true;
    }
    if(stp != null && stp < stamp) {
    
    
        return false;
    }
    //每个已获得资源的事务一个独特的stamp
    xidStamp.put(xid, stamp);
    System.out.println("xidStamp找不到该xid,加入后xidStamp变为"+xidStamp);
    //已获得资源的事务xid正在等待的uid
    Long uid = waitU.get(xid);
    System.out.println("xid"+xid+"正在等待的uid是"+uid);
   if(uid == null){
    
    
       System.out.println("未成环,退出深搜");
       //xid没有需要等待的uid,无死锁
       return false;
   }
    //xid需要等待的uid被哪个xid占用了
    Long x = u2x.get(uid);
    System.out.println("xid"+xid+"需要的uid被"+"xid"+x+"占用了");
    System.out.println("=====再次进入深搜"+"xid"+x+"====");
    assert x != null;
    return dfs(x);
}

Cuando una transacción se confirma o aborta, libera todos los bloqueos que tiene y se elimina del gráfico de espera.

public void remove(long xid) {
    
    
    lock.lock();
    try {
    
    
        List<Long> l = x2u.get(xid);
        if(l != null) {
    
    
            while(l.size() > 0) {
    
    
                Long uid = l.remove(0);
                selectNewXID(uid);
            }
        }
        waitU.remove(xid);
        x2u.remove(xid);
        waitLock.remove(xid);

    } finally {
    
    
        lock.unlock();
    }
}

El bucle while libera todos los bloqueos sobre los recursos retenidos por este subproceso, que pueden adquirirse mediante subprocesos en espera:

// 从等待队列中选择一个xid来占用uid
private void selectNewXID(long uid) {
    
    
    u2x.remove(uid);
    List<Long> l = wait.get(uid);
    if(l == null) return;
    assert l.size() > 0;

    while(l.size() > 0) {
    
    
        long xid = l.remove(0);
        if(!waitLock.containsKey(xid)) {
    
    
            continue;
        } else {
    
    
            u2x.put(uid, xid);
            Lock lo = waitLock.remove(xid);
            waitU.remove(xid);
            lo.unlock();
            break;
        }
    }

    if(l.size() == 0) wait.remove(uid);
}

Sigue siendo un candado justo para intentar desbloquear desde el principio de la Lista. Al desbloquear, simplemente desbloquee el objeto de bloqueo, de modo que el subproceso comercial haya adquirido el bloqueo y pueda continuar ejecutándose.

El código de prueba es el siguiente:

public static void main(String[] args) throws Exception {
    
    
    LockTable lock = new LockTable();
    lock.add(1L,3L);
    lock.add(2L,4L);
    lock.add(3L,5L);
    lock.add(1L,4L);

    System.out.println("+++++++++++++++++++++++");
    lock.add(2L,5L);
    System.out.println("++++++++++++++++");
    lock.add(3L,3L);
    System.out.println(lock.hasDeadLock());
}

Los resultados de la ejecución son los siguientes:

xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是null
未成环,退出深搜
xid3的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=3}
xid3正在等待的uid是null
未成环,退出深搜
+++++++++++++++++++++++
xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=2}
xid3正在等待的uid是null
未成环,退出深搜
++++++++++++++++
xid已经持有哪些uid x2u={
    
    1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={
    
    3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xidStamp找不到该xid,加入后xidStamp变为{
    
    1=2, 2=2, 3=2}
xid3正在等待的uid是3
xid3需要的uid被xid1占用了
=====再次进入深搜xid1====

Cómo hacer el aislamiento de transacciones en escenarios de alta concurrencia

Análisis funcional del nivel de aislamiento de transacciones

Se utilizan bloqueos y tecnología de control de versiones múltiples (MVCC) para garantizar el aislamiento. Hay cuatro tipos de aislamiento admitidos por InnoDB, de bajo a alto.

  • Lectura no confirmada (LECTURA NO COMPROMETIDA)
  • LEER COMPROMETIDO
  • Lectura repetible (LECTURA REPETIBLE)
  • Serializable (SERIALIZABLE).

Lectura no confirmada (LECTURA NO COMPROMETIDA)

Bajo el nivel de aislamiento de lectura no confirmada, se permiten lecturas sucias. No comprometido significa que estos datos se pueden revertir y se leen los datos que no necesariamente terminan siendo leídos. El nivel de aislamiento de lectura no confirmada es propenso a problemas de lectura sucia;

Lectura sucia significa que bajo diferentes transacciones, la transacción actual puede leer datos no confirmados de otras transacciones, lo que simplemente significa leer datos sucios. Los llamados datos sucios se refieren a la modificación del registro de fila en el grupo de búfer por parte de la transacción, y aún no se ha enviado.

Si se leen datos sucios, es decir, una transacción puede leer datos no confirmados en otra transacción, lo que obviamente viola el aislamiento de la base de datos.

La condición para que se produzcan lecturas sucias es que el nivel de aislamiento de la transacción debe ser Lectura no confirmada. En un entorno de producción, la mayoría de las bases de datos se establecerán al menos en Lectura confirmada, por lo que la probabilidad de envío en un entorno de producción es muy pequeña.

LEER COMPROMETIDO

Solo se permite leer los datos enviados, es decir, en el proceso de transacción A acumulando n de 0 a 10, B no puede ver el valor chino de n, pero solo puede ver 10.

Bajo el nivel de aislamiento comprometido, las lecturas sucias están prohibidas, pero se ejecutan las lecturas no repetibles.

La lectura confirmada satisface la definición simple de aislamiento: una transacción solo puede ver los cambios realizados por las transacciones confirmadas.

El nivel de aislamiento predeterminado de Oracle y el servidor SQL es lectura confirmada.

El nivel de aislamiento de lectura confirmada es propenso a problemas de lectura no repetibles. Las lecturas no repetibles se refieren a la situación en la que los datos del mismo conjunto de datos se leen varias veces dentro de una transacción.


La transacción A lee los mismos datos varias veces, pero la transacción B actualiza y compara los datos durante las lecturas múltiples de la transacción A, lo que genera resultados incoherentes cuando la transacción A lee los mismos datos varias veces.

La diferencia entre las lecturas sucias y las lecturas no repetibles es: las lecturas sucias leen datos no confirmados, mientras que las lecturas no repetibles leen datos confirmados, pero violan el principio de coherencia de las transacciones de la base de datos.

En circunstancias normales, el problema de la lectura no repetible es aceptable, porque lee los datos que se han enviado y no causará un gran problema en sí mismo. El nivel de aislamiento predeterminado de las transacciones de la base de datos del servidor Oracle y SQL se establece en Lectura confirmada, y el nivel de aislamiento RC es un fenómeno que permite la lectura no repetible. El nivel de aislamiento predeterminado de Mysql es RR, y el fenómeno de lectura no repetible se evita mediante el uso del algoritmo Next-Key Lock.

La diferencia entre lectura no repetible y lectura fantasma es:

  • El punto clave de la lectura no repetible es la modificación: en las mismas condiciones, los datos que se leyeron por primera vez se vuelven a leer y el valor es diferente.
  • El enfoque de la lectura fantasma es agregar o eliminar: bajo las mismas condiciones, el número de registros leídos por primera vez y por segunda vez es diferente.

Lectura repetible (LECTURA REPETIBLE)

Se garantiza que cuando los mismos datos se leen varias veces durante la transacción, su valor es consistente con el del inicio de la transacción.
Bajo el nivel de aislamiento de lectura repetible, las lecturas sucias y las lecturas no repetibles están prohibidas, pero existen lecturas fantasma.

La lectura fantasma significa que la cantidad de datos en las dos consultas de una transacción es inconsistente. Por ejemplo, una transacción consulta varias columnas (Fila) de datos, mientras que otra transacción inserta nuevas columnas de datos en este momento. En la siguiente consulta, encontrará que hay varias columnas de datos que no tenía antes.

El nivel de aislamiento predeterminado de mysql es lectura repetible. Para ver el nivel de aislamiento de transacciones de la base de datos actual de mysql, el comando es el siguiente

show variables like 'tx_isolation';select @@tx_isolation;

Establezca el nivel de aislamiento de la transacción:

set tx_isolation='READ-UNCOMMITTED';
set tx_isolation='READ-COMMITTED';
set tx_isolation='REPEATABLE-READ';
set tx_isolation='SERIALIZABLE';

En teoría, la lectura repetible conducirá a otro problema espinoso: la lectura fantasma.La lectura fantasma hasta que el usuario actual lea un rango de filas de datos, otra transacción inserte nuevas filas en el rango y cuando el usuario lea Cuando hay filas de datos en este rango , se encontrarán nuevas filas "fantasmas". El motor de almacenamiento InnoDB resuelve este problema a través del mecanismo de control de concurrencia multiversión (MVCC, Multiversion Concurrency Control).

Serializable (SERIALIZABLE)

La transacción más estricta requiere que todas las transacciones se ejecuten en serie y no se pueden ejecutar simultáneamente.

Resuelve el problema de la lectura fantasma al hacer cumplir el orden de las transacciones para que no entren en conflicto entre sí.

En otras palabras, se agrega un bloqueo compartido a cada fila de datos leídos.

Serializable puede causar tiempos de espera masivos y contención de bloqueo.

Resumen :

nivel de aislamiento lectura sucia Lectura no repetible (NoRepeatable Read) Lectura fantasma
Lectura no confirmada (Lectura no confirmada) posible posible posible
Leer comprometido imposible posible posible
Lectura repetible (Lectura repetible) imposible imposible posible
Serializable imposible imposible imposible

MVCC

El nombre completo de MVCC es Control concurrente de múltiples versiones, es decir, control de concurrencia de múltiples versiones, y su principio es similar a copyonwrite.

Problemas causados ​​por transacciones concurrentes

leer leer

Es decir, las transacciones simultáneas leen el mismo registro una tras otra.

Debido a que leer un registro no tiene ningún impacto en el registro, no hay problema de seguridad al leer el mismo registro simultáneamente por la misma transacción, por lo que esta operación está permitida. No hay problema ni necesidad de control de concurrencia.

escribe escribe

Es decir, transacciones concurrentes modifican sucesivamente el mismo registro.

Si se permite que las transacciones concurrentes lean el mismo registro y realicen modificaciones a este registro en función de la estimación anterior, entonces la modificación realizada por la transacción anterior se sobrescribirá con la modificación de la transacción posterior, es decir, se produce el problema de la cobertura de compromiso. .

En otro caso, las transacciones concurrentes realizan modificaciones al mismo registro una tras otra, y la otra transacción retrocede después de que se confirma una transacción, por lo que habrá un problema de que la modificación confirmada se pierde debido a la reversión, es decir, la cobertura de reversión problema.

Entonces, hay un problema de seguridad de subprocesos y puede haber un problema de pérdida de actualización en ambos casos.

escribir-leer o leer-escribir

Es decir, dos transacciones simultáneas realizan operaciones de lectura y escritura en el mismo registro respectivamente.

Si una transacción lee un registro de revisión que no ha sido confirmado por otra transacción, entonces hay un problema de lectura sucia ;

Si lo controlamos para que una transacción solo pueda leer los datos modificados de otras transacciones comprometidas, entonces los datos leídos por esta transacción antes y después de que otra transacción confirme la modificación son diferentes, lo que significa que se ha producido una lectura no repetible ;

Si una transacción encuentra algunos registros de acuerdo con algunas condiciones, y luego otra cosa inserta algunos registros en la tabla, la transacción original encuentra que los resultados obtenidos cuando se consultan las mismas condiciones nuevamente son inconsistentes con los resultados obtenidos por la primera consulta, lo que significa Entonces ocurrió la lectura fantasma .

Para los problemas de escritura-lectura o lectura-escritura, InnoDB de MySQL implementa MVCC para manejar mejor los conflictos de lectura-escritura Incluso si hay lecturas y escrituras simultáneas, no hay necesidad de bloquear y realizar "lecturas simultáneas sin bloqueo".

Entonces, para resumir, la razón por la que se necesita MVCC es que las bases de datos generalmente usan bloqueos para lograr el aislamiento.Los bloqueos más primitivos, después de bloquear un recurso, prohibirán que cualquier otro subproceso acceda al mismo recurso. Sin embargo, una característica de muchas aplicaciones es el escenario de más lecturas y menos escrituras. El número de lecturas de una gran cantidad de datos es mucho mayor que el número de modificaciones, y la exclusión mutua entre los datos de lectura no es necesaria. Por lo tanto, una lectura- Se utiliza el método de bloqueo de escritura. Los bloqueos de lectura y los bloqueos de lectura no se excluyen mutuamente, mientras que los bloqueos de escritura, los bloqueos de escritura y los bloqueos de lectura se excluyen mutuamente. Esto mejora enormemente la capacidad de concurrencia del sistema.

Más tarde, las personas descubrieron que la lectura simultánea no era suficiente y propusieron un método para evitar conflictos entre la lectura y la escritura, que consiste en guardar los datos de una manera similar a las instantáneas cuando se leen datos, de modo que el bloqueo de lectura no entre en conflicto con el de escritura. Sí, las diferentes sesiones de transacciones verán su propia versión específica de los datos. Por supuesto, la instantánea es un modelo conceptual y diferentes bases de datos pueden implementar esta función de diferentes maneras.

Bajo el protocolo MVCC, cada operación de lectura verá una instantánea consistente y se pueden lograr lecturas sin bloqueo. Lea esta instantánea instantánea sin bloquear. Además de la versión instantánea instantánea, MVCC permite que los datos tengan varias versiones. El número de versión puede ser una marca de tiempo o un
ID de transacción incrementado globalmente. En el mismo momento, diferentes transacciones ven datos diferentes.

En primer lugar, antes de entender MVCC, debemos aclarar dos definiciones

  • Lectura actual : los datos leídos son la última versión. Al leer los datos, también es necesario asegurarse de que otras transacciones concurrentes no modifiquen los datos actuales. La lectura actual bloqueará los registros leídos. Por ejemplo: seleccione... bloquear en modo compartido (bloqueo compartido), seleccione... para actualizar | actualizar | insertar | borrar (bloqueo exclusivo)
  • Lectura de instantáneas : cada vez que se modifican los datos, se almacenará un registro de instantáneas en el registro de deshacer. La instantánea aquí es para leer una instantánea de una determinada versión en el registro de deshacer. La ventaja de este método es que los datos se pueden leer sin bloqueo, pero la desventaja es que los datos leídos pueden no ser la última versión. Las consultas generales son lecturas de instantáneas, por ejemplo: select * from t_user where id=1; las consultas en MVCC son todas instantáneas.

El principio de realización de mvcc

MVCC en MySQL se implementa principalmente a través de campos ocultos en registros de fila (clave primaria oculta ID_fila, ID de transacción trx_id, puntero de reversión roll_pointer), registro de deshacer (cadena de versión) y ReadView (vista de lectura consistente).

campo escondido

En MySQL, además de los campos personalizados, existen algunos campos ocultos en cada fila de registros:

  • row_id : cuando la tabla de la base de datos no define una clave principal, InnoDB generará un índice agrupado con row_id como clave principal.
  • trx_id : el ID de transacción registra el ID de transacción del registro recién agregado/modificado recientemente, y el ID de transacción se incrementa automáticamente.
  • roll_pointer : el puntero de reversión apunta a la versión anterior del registro actual (en el registro de deshacer).

Vista de lectura

La vista de coherencia ReadView se compone principalmente de dos partes: la matriz de ID de todas las transacciones no confirmadas y el ID de transacción más grande que se ha creado. Por ejemplo: [100,200],300. Las transacciones 100 y 200 son actualmente transacciones no comprometidas, mientras que la transacción 300 es la transacción más grande creada actualmente (ya comprometida). ReadView se crea cuando se ejecuta la instrucción SELECT, pero la estrategia para generar ReadView es diferente en los dos niveles de transacción de lectura confirmada y lectura repetible: el nivel de lectura confirmada se regenera cada vez que se ejecuta la instrucción SELECT. El nivel solo se genera cuando se ejecuta la primera instrucción SELECT, y la instrucción SELECT subsiguiente continuará usando el ReadView generado anteriormente (incluso si hay una declaración de actualización más tarde, continuará usándose).

ReadView es una "vista de lectura" que MVCC genera cuando se toman instantáneas de datos. Hay 4 variables más importantes en ReadView:

  • m_ids : lista de ID de transacciones activas, la lista de ID de transacciones de todas las transacciones activas (es decir, no confirmadas) en el sistema actual.
  • min_trx_id : la identificación de transacción más pequeña en m_ids.
  • max_trx_id : al generar ReadView, el sistema debe asignar la identificación a la siguiente transacción (tenga en cuenta que no es la identificación de transacción más grande en m_ids), es decir, la identificación de transacción más grande en m_ids + 1.
  • Creator_trx_id : El id de transacción de la transacción que generó este ReadView.

cadena de versiones

Al modificar datos, el contenido de la página modificada se registrará en el registro de rehacer (para restaurar el funcionamiento de la base de datos después de que se reinicie la base de datos), y la instantánea original de los datos se registrará en el registro de deshacer (para revertir la operación de la base de datos). transacción). El registro de deshacer tiene dos funciones: además de usarse para revertir transacciones, también se usa para implementar MVCC.

Use un ejemplo simple para dibujar el diagrama lógico de la cadena de versión de registro de deshacer que se usa en MVCC:

Cuando se ejecuta la transacción 100 (trx_id=100) se inserta en t_user valores(1,'bend',30); después:

cadena de versiones de mysql

Cuando la transacción 102 (trx_id=102) ejecuta la actualización t_user set name='Li Si' where id=1; Después:

cadena de versiones de mysql

Cuando la transacción 102 (trx_id=102) ejecuta la actualización t_user set name='Wang Wu' where id=1; después:

cadena de versiones de mysql

Las reglas de comparación de la cadena de versión específica son las siguientes: Primero, saque el ID de transacción de la primera versión en la parte superior de la cadena de versión y comience a comparar uno por uno:

cadena de versión específica

(donde min_id apunta al ID de transacción más pequeño en la matriz de transacciones no confirmadas en ReadView y max_id apunta al ID de transacción más grande que se ha creado en ReadView)

Qué versión de datos se puede leer cuando una transacción realiza una lectura de instantáneas, las reglas de ReadView son las siguientes:

(1) Cuando el trx_id registrado en la cadena de versiones es igual al id de la transacción actual (trx_id = Creator_trx_id) , significa que esta versión en la cadena de versiones es modificada por la transacción actual, por lo que el registro de la instantánea es visible para la transacción actual. .

(2) Cuando el trx_id registrado en la cadena de versiones es menor que el id mínimo de la transacción activa (trx_id < min_trx_id) , significa que el registro en la cadena de versiones se ha enviado, por lo que el registro de la instantánea es visible para la transacción actual. .

(3) Cuando el trx_id registrado en la cadena de versión es mayor que el siguiente id de transacción que se asignará (trx_id > max_trx_id) , el registro de la instantánea no es visible para la transacción actual.

(4) Cuando el trx_id registrado en la cadena de versiones es mayor o igual que el id mínimo de transacción activa y el trx_id registrado en la cadena de versiones es más pequeño que el siguiente id de transacción que se asignará (min_trx_id<= trx_id < max_trx_id), si el trx_id registrado en la cadena de versión está activo En la lista de id de transacción m_ids, indica que cuando se genera ReadView, la transacción que modifica el registro no se ha enviado, por lo que el registro de instantánea no es visible para la transacción actual; de lo contrario, el el registro de instantánea es visible para la transacción actual.

Cuando la transacción lea la instantánea del registro con id=1, seleccione * de t_user donde id=1, en la instantánea de la cadena de versión, comenzando desde el último registro, juzgue estas 4 condiciones por turno, hasta una determinada versión del la instantánea es correcta para la transacción actual La transacción es visible; de ​​lo contrario, continúe comparando la versión anterior del registro.

MVCC se usa principalmente para resolver el problema de lecturas sucias bajo el nivel de aislamiento RU y lecturas no repetibles bajo el nivel de aislamiento RC, por lo que MVCC solo tiene efecto bajo el aislamiento RC (resolver lecturas sucias) y RR (resolver lecturas no repetibles) niveles, es decir, MySQL solo generará ReadView para lecturas de instantáneas en los niveles de aislamiento RC y RR.

La diferencia es que, con el nivel de aislamiento RC, cada lectura de instantánea generará una vista de lectura más reciente; con el nivel de aislamiento de RR, solo la lectura de la primera instantánea en la transacción generará una vista de lectura, y las lecturas de instantáneas posteriores utilizarán la vista de lectura generada para la primera. tiempo. .

MySQL reduce la probabilidad de bloqueo de transacciones a través de MVCC.

Por ejemplo: T1 desea actualizar el valor del registro X, por lo que T1 necesita adquirir primero el bloqueo de X y luego actualizar, es decir, crear una nueva versión de X, asumiendo que es x3.

Suponiendo que T1 no haya liberado el bloqueo de X, T2 desea leer el valor de X y no se bloqueará en este momento, y MYDB devolverá una versión anterior de X, como x2. De esta forma, el resultado final de la ejecución es equivalente a que T2 se ejecuta primero, y T1 se ejecuta después, y la secuencia de programación aún es serializable. Si X no tiene una versión anterior, solo puede esperar a que T1 libere el bloqueo.

Así que solo reduce la probabilidad.

nivel de aislamiento de transacciones manuscritas

Si la última versión de un registro está bloqueada, cuando otra transacción quiera modificar o leer este registro, MYDB devolverá una versión anterior de los datos.

En este momento, se puede considerar que la última versión bloqueada es invisible para otra transacción.

Comprometidos a leer

Es decir, cuando una transacción lee datos, solo puede leer los datos generados por la transacción confirmada.

La visibilidad de la versión está relacionada con el aislamiento de transacciones.

El grado más bajo de aislamiento de transacciones admitido por MYDB es "Lectura confirmada", es decir, cuando una transacción lee datos, solo puede leer los datos generados por la transacción confirmada. Los beneficios de garantizar la confirmación de lectura más baja se han explicado en el Capítulo 4 (evitar que las reversiones en cascada entren en conflicto con la semántica de confirmación).

MYDB implementa el envío de lectura y mantiene dos variables para cada versión, que son las mencionadas XMIN y XMAX:

  • XMIN : el número de transacción que creó esta versión
  • XMAX : elimine el número de transacción para esta versión

XMIN debe completarse cuando se crea una versión y XMAX debe completarse cuando se elimina una versión o aparece una nueva.

La variable XMAX también explica por qué la capa DM no proporciona una operación de eliminación. Cuando desee eliminar una versión, solo necesita configurar su XMAX. De esta manera, esta versión es invisible para cada transacción después de XMAX, lo que equivale a fué borrado.

En la confirmación de lectura, la lógica de visibilidad de la versión de la transacción es la siguiente:

(XMIN == Ti and                             // 由Ti创建且
    XMAX == NULL                            // 还未被删除
)
or                                          // 或
(XMIN is commited and                       // 由一个已提交的事务创建且
    (XMAX == NULL or                        // 尚未删除或
    (XMAX != Ti and XMAX is not commited)   // 由一个未提交的事务删除
))

Si la condición es verdadera, la versión es visible para Ti. Luego, para obtener la versión adecuada para Ti, solo necesita comenzar desde la última versión y verificar la visibilidad hacia adelante uno por uno. Si es cierto, puede regresar directamente.

El siguiente método determina si un registro es visible para la transacción t:

private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();
    if(xmin == xid && xmax == 0) return true;

    if(tm.isCommitted(xmin)) {
    
    
        if(xmax == 0) return true;
        if(xmax != xid) {
    
    
            if(!tm.isCommitted(xmax)) {
    
    
                return true;
            }
        }
    }
    return false;
}

lectura repetible

No repetibilidad, lo que hará que una transacción obtenga resultados diferentes al leer el mismo elemento de datos durante la ejecución. Como se muestra en los siguientes resultados, el valor inicial de sumar X es 0:


T1 begin
R1(X) // T1 读得 0
T2 begin
U2(X) // 将 X 修改为 1
T2 commit
R1(X) // T1 读的 

Se puede ver que T1 lee X dos veces y los resultados de lectura son diferentes. Si desea evitar esta situación, debe introducir un nivel de aislamiento más estricto, es decir, lectura repetible.

Cuando T1 lee por segunda vez, lee el valor modificado por T2 que se envió (esta transacción de modificación es la versión original de XMAX y la nueva versión de XMIN), lo que genera este problema. Entonces podemos estipular que una transacción solo puede leer las versiones de datos generadas por aquellas transacciones que terminaron cuando comenzó.

De acuerdo con esta regla, la transacción debe ignorar 2 puntos:

(1) Datos de transacciones iniciadas después de esta transacción;

(2) Los datos de la transacción que todavía estaba activa cuando comenzó la transacción.

Para el primer artículo, solo necesita comparar el ID de la transacción para determinarlo. En cuanto al segundo elemento, es necesario registrar todas las transacciones actualmente activas SP(Ti) al comienzo de la transacción Ti. Si se registra una determinada versión, XMIN debería ser invisible para Ti en SP(Ti).

Por lo tanto, la lógica de juicio de la lectura repetible es la siguiente:

(XMIN == Ti and                 // 由Ti创建且
 (XMAX == NULL or               // 尚未被删除
))
or                              // 或
(XMIN is commited and           // 由一个已提交的事务创建且
 XMIN < XID and                 // 这个事务小于Ti且
 XMIN is not in SP(Ti) and      // 这个事务在Ti开始前提交且
 (XMAX == NULL or               // 尚未被删除或
  (XMAX != Ti and               // 由其他事务删除但是
   (XMAX is not commited or     // 这个事务尚未提交或
XMAX > Ti or                    // 这个事务在Ti开始之后才开始或
XMAX is in SP(Ti)               // 这个事务在Ti开始前还未提交
))))

Por lo tanto, es necesario proporcionar una estructura para abstraer una transacción para guardar los datos de la instantánea (la transacción aún estaba activa cuando se creó):

// vm对一个事务的抽象
public class Transaction {
    
    
    public long xid;
    public int level;
    public Map<Long, Boolean> snapshot;
    public Exception err;
    public boolean autoAborted;

    //事务id  隔离级别  快照
    public static Transaction newTransaction(long xid, int level, Map<Long, Transaction> active) {
    
    
        Transaction t = new Transaction();
        t.xid = xid;
        t.level = level;
        if(level != 0) {
    
    
            //隔离级别为可重复读,读已提交不需要快照信息
            t.snapshot = new HashMap<>();
            for(Long x : active.keySet()) {
    
    
                t.snapshot.put(x, true);
            }
        }
        return t;
    }

    public boolean isInSnapshot(long xid) {
    
    
        if(xid == TransactionManagerImpl.SUPER_XID) {
    
    
            return false;
        }
        return snapshot.containsKey(xid);
    }
}

El activo en el método de construcción guarda todas las transacciones activas actuales.

Por lo tanto, bajo el nivel de aislamiento de lectura repetible, el juicio de si una versión es visible para la transacción es el siguiente:

private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xid = t.xid;
    long xmin = e.getXmin();
    long xmax = e.getXmax();
    if(xmin == xid && xmax == 0) return true;
 
    if(tm.isCommitted(xmin) && xmin < xid && !t.isInSnapshot(xmin)) {
    
    
        if(xmax == 0) return true;
        if(xmax != xid) {
    
    
            if(!tm.isCommitted(xmax) || xmax > xid || t.isInSnapshot(xmax)) {
    
    
                return true;
            }
        }
    }
    return false;
}

salto de versión

Para el problema de salto de versión, considere la siguiente situación, suponiendo que X inicialmente solo tiene la versión x0, y que tanto T1 como T2 son niveles de aislamiento de lectura repetibles:

T1 begin
T2 begin
R1(X) // T1读取x0
R2(X) // T2读取x0
U1(X) // T1将X更新到x1
T1 commit
U2(X) // T2将X更新到x2
T2 commit

Esta situación está bien en la práctica, pero no es lógicamente correcta.

T1 actualiza X de x0 a x1, lo cual es correcto. Pero T2 actualiza X de x0 a x2, omitiendo la versión x1.

Las confirmaciones de lectura permiten saltos de versión, mientras que las lecturas repetibles no permiten saltos de versión.

La idea para resolver el salto de versión: si Ti necesita modificar X, y X ha sido modificado por la transacción Tj invisible para Ti, entonces se requiere que Ti retroceda.

La implementación de MVCC hace posible deshacer o revertir una transacción: solo es necesario marcar esta transacción como abortada.

De acuerdo con la visibilidad mencionada en el capítulo anterior, cada transacción solo puede ver los datos generados por otras transacciones comprometidas, y los datos generados por una transacción abortada no tendrán ningún impacto en otras transacciones, lo que equivale a, Esta transacción nunca existió

if(Visibility.isVersionSkip(tm, t, entry)) {
    
    
    System.out.println("检查到版本跳跃,自动回滚");
    t.err = Error.ConcurrentUpdateException;
    internAbort(xid, true);
    t.autoAborted = true;
    throw t.err;
}

Hemos resumido anteriormente que hay dos situaciones en las que Ti es invisible para Tj:

(1) XID(Tj) > XID(Ti) Versión modificada creada después de Ti

(2) Cuando se crea Tj en SP(Ti) Ti, la versión modificada se ha creado pero aún no se ha enviado

La verificación de salto de versión primero saca la última versión enviada de los datos X que se van a modificar y verifica si el creador de la última versión es visible para la transacción actual. La implementación específica es la siguiente:

public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) {
    
    
    long xmax = e.getXmax();
    if(t.level == 0) {
    
    
        return false;
    } else {
    
    
        return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax));
    }
}

Sobre el Autor:

Primer trabajo: Mark , arquitecto sénior de big data, arquitecto de Java, casi 20 años de experiencia en Java, arquitectura y desarrollo de big data. Mentor sénior de arquitectura, guió con éxito múltiples posiciones intermedias de arquitecto de transformación de Java y Java sénior.

Segundo trabajo: Nien , arquitecto sénior de sistemas, escritor sénior en el campo de TI y famoso bloguero. En los últimos 20 años, ha trabajado en investigación de arquitectura de 3 niveles, arquitectura de sistemas, análisis de sistemas y desarrollo de código central en los campos de plataforma web de alto rendimiento, comunicación de alto rendimiento, búsqueda de alto rendimiento y minería de datos. Mentor sénior de arquitectura, guió con éxito múltiples posiciones intermedias de arquitecto de transformación de Java y Java sénior.

Dijo en la parte de atrás:

La iteración continua y la actualización continua son los principios del equipo de Nien.

La iteración continua y la actualización continua también son el alma de "A partir de 0, escribir a mano MySQL".

Se recopilarán más preguntas reales de la entrevista más adelante. Al mismo tiempo, si tiene problemas con la entrevista, puede acudir a la comunidad de Nien "Technical Freedom Circle (anteriormente Crazy Maker Circle)" para comunicarse y pedir ayuda.

Nuestro objetivo es crear la mejor colección de entrevistas "Manuscrito MySQL" del mundo.

El camino de realización de la libertad técnica PDF:

Realice su libertad arquitectónica:

" Tenga un conocimiento profundo de la plantilla de 8 cifras 1, todos pueden hacer la arquitectura "

Plataforma de revisión 10Wqps , ¿cómo estructurarla? ¡Esto es lo que hace la estación B! ! ! "

" Alibaba Two Sides: ¿Cómo optimizar el rendimiento de decenas de millones y miles de millones de datos?" Las respuestas a nivel de libro de texto están llegando "

" Pico 21WQps, 100 millones DAU, ¿cómo se estructura el juego pequeño "Sheep a Sheep"? "

" Cómo programar pedidos de 10 mil millones de niveles, llegar a la excelente solución de una gran fábrica "

" Esquema de arquitectura de sobre rojo de dos grandes fábricas de 10 mil millones de niveles "

… más artículos de arquitectura, siendo agregados

Realice su libertad de respuesta:

" Responsive Bible: 10W Words, Realize Spring Responsive Programming Freedom "

Esta es la versión antigua de " Flux, Mono, Reactor Combat (la más completa de la historia) "

Realice su libertad de nube de primavera:

" Nube de primavera Biblia de estudio de Alibaba "

" Principio subyacente y práctica central de Sharding-JDBC (la más completa de la historia) "

" Hágalo en un artículo: la caótica relación entre SpringBoot, SLF4j, Log4j, Logback y Netty (la más completa de la historia) "

Realice su libertad de Linux:

" Enciclopedia de comandos de Linux: 2W más palabras, una vez para lograr la libertad de Linux "

Realice su libertad en línea:

" Explicación detallada del protocolo TCP (el más completo de la historia) "

" Tres tablas de red: tabla ARP, tabla MAC, tabla de enrutamiento, ¡haga realidad su libertad de red!" ! "

Realice su libertad de bloqueo distribuido:

" Redis Distributed Lock (Ilustración - Segundo Entendimiento - El Más Completo de la Historia) "

" Bloqueo distribuido de Zookeeper - Diagrama - Segundo entendimiento "

Realice su libertad componente rey:

" Rey de la cola: principios disruptivos, arquitectura y penetración del código fuente "

" El rey de la memoria caché: código fuente, arquitectura y principios de la cafeína (el más completo de la historia, texto superlargo de 10 W) "

" El rey del caché: el uso de la cafeína (el más completo de la historia) "

" Java Agent probe, bytecode mejorado ByteBuddy (el más completo de la historia) "

Realice las preguntas de su entrevista libremente:

4000 páginas de "Nin's Java Interview Collection" 40 temas

Vaya a la siguiente cuenta oficial de "Technical Freedom Circle" para obtener la actualización del archivo PDF de las notas de arquitectura de Nien y las preguntas de la entrevista↓↓↓

Supongo que te gusta

Origin blog.csdn.net/crazymakercircle/article/details/131297906
Recomendado
Clasificación