Knex+MySQL UPDATE entonces SELECT algunas veces devuelve valores antiguos

El siguiente código algunas veces devuelve valores antiguos de MySQL utilizando Knex.js:

await db.knex('table')
  .where('id', '=', table_id)
  .update({
    last_opening: db.knex.fn.now(),
  });

// ... removed another db.knex('table')...update() here for brevity, updating different fields

const updated_data = await db.knex('table')
  .where('id', '=', table_id)
  .select('last_opening')
  .first();

// At this point updated_data.last_opening randomly contains either the old or the new date

db.knex viene de otro módulo como este:

// package.json has "knex": "^0.21.1",
const knex_factory = require('knex');

exports.knex = knex_factory({
  client: 'mysql',
  connection: {
    host : conf.DB_HOST,
    user : conf.DB_USER,
    password : conf.DB_PASSWORD,
    database : conf.DB_NAME,
  }
});

Parece que sólo ocurre en el entorno de producción. No he sido capaz de reproducir localmente en absoluto, y no puedo recordar si lo vi en el entorno de estancamiento.

$ SELECT @@TX_ISOLATION;
REPEATABLE-READ

$ SELECT VERSION();
5.7.32-0ubuntu0.16.04.1

Esta pregunta InnoDB / MySQL - nueva transacción utiliza datos antiguos sobre SELECT en lugar de devolver datos actualizados me lleva a sospechar que esto tiene que ver con los niveles de aislamiento de transacción y la piscina de conexión de Knex, pero no entiendo lo suficiente para cambiar cómodamente nada.

¿Alguien puede ayudar a explicar lo que está pasando?

Pregunta hecha hace 3 años, 4 meses, 28 días - Por compilerhero


3 Respuestas:

  • No creo que el código debería / podría devolver el valor antiguo a menos que esté ejecutando ese código varias veces simultáneamente, cuando por ejemplo 2 manipuladores de solicitud pueden actualizar la fila uno tras otro y luego ambos están leyendo la última actualización de la DB.

    También si tienes múltiples procesos ejecutando ese código podría causar ese tipo de comportamiento.

    Si envuelves ambas consultas para estar en una transacción que no debe suceder incluso si las actualizaciones se ejecutan simultáneamente.

    Respondida el Dec 18, 2020 a las 11:54 - por debugdynamo46a0

    Votos positivos: 0 | Votos negativos: 0

  • El problema que estás experimentando parece estar relacionado con el nivel de aislamiento de transacciones en MySQL y cómo interactúa con Knex.js y la piscina de conexiones.

    Por defecto, el nivel de aislamiento de transacciones en MySQL es REPEATABLE-READ, lo que significa que las lecturas en una transacción obtienen una vista consistente de los datos en el momento en que inició la transacción. Sin embargo, si una transacción realiza una actualización en una fila y otra transacción realiza una lectura en la misma fila antes de que se haya confirmado la actualización, la segunda transacción podría ver los datos antiguos.

    En tu caso, al realizar una actualización en una fila y luego una lectura inmediatamente después mediante Knex.js, es posible que esta lectura se realice en una conexión diferente en la piscina de conexiones, lo que podría explicar por qué a veces obtienes valores antiguos.

    Una posible solución sería cambiar el nivel de aislamiento de transacciones en MySQL a READ-COMMITTED, que garantiza que las lecturas obtengan datos actualizados en lugar de valores antiguos. Esto se puede hacer ejecutando el siguiente comando en MySQL:

    
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    

    También puedes configurar Knex.js para que use una única conexión en lugar de una piscina de conexiones, lo que podría evitar este problema. Puedes hacerlo modificando la configuración de Knex.js al crear la instancia:

    javascript
    const knex = require('knex')({
      client: 'mysql',
      connection: {
        host : conf.DB_HOST,
        user : conf.DB_USER,
        password : conf.DB_PASSWORD,
        database : conf.DB_NAME,
      },
      pool: { min: 0, max: 1 } // Configuración para una única conexión
    });
    

    Espero que esta información te ayude a resolver el problema de obtener valores antiguos en tus consultas. ¡Déjame saber si tienes alguna otra duda!

    Respondida el Dec 19, 2020 a las 11:51 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • Problema de lectura fantasma:

    El problema en tu código es un problema de lectura fantasma. En MySQL, con el nivel de aislamiento predeterminado (READ COMMITTED), las lecturas pueden mostrar datos comprometidos recientemente por otras transacciones, incluso si la transacción actual no los ha comprometido.

    Causa:

    El problema con el código proporcionado es que las dos consultas de actualización se ejecutan en transacciones separadas debido a la piscina de conexión de Knex. Esto significa que:

    • La primera consulta actualiza la columna `last_opening`.
    • La segunda consulta de actualización se ejecuta en una nueva transacción, que aún no ve la actualización realizada por la primera transacción.
    • La consulta de selección se ejecuta en la misma transacción que la segunda consulta de actualización, por lo que devuelve el valor antiguo de `last_opening`.

    Solución:

    Para resolver este problema, debes asegurarte de que la consulta de selección se ejecute en la misma transacción que la consulta de actualización. Hay dos formas de hacerlo:

    1. Usar una transacción explícita:

    js
    await db.knex.transaction(async (trx) => {
      await trx('table')
        .where('id', '=', table_id)
        .update({
          last_opening: trx.fn.now(),
        });
    
      const updated_data = await trx('table')
        .where('id', '=', table_id)
        .select('last_opening')
        .first();
    });
    

    Esta solución asegura que la consulta de selección se ejecute dentro de la misma transacción que la consulta de actualización, por lo que siempre verá los datos actualizados.

    2. Cambiar el nivel de aislamiento de la conexión:

    Puedes cambiar el nivel de aislamiento de la conexión a SERIALIZABLE, que garantiza que las transacciones no vean los cambios realizados por otras transacciones hasta que se comprometan.

    js
    exports.knex = knex_factory({
      client: 'mysql',
      connection: {
        ...conf.DB_CONNECTION,
        isolationLevel: 'SERIALIZABLE',
      },
    });
    

    Nota: cambiar el nivel de aislamiento a SERIALIZABLE puede tener un impacto en el rendimiento, por lo que debes considerarlo cuidadosamente.

    Respondida el Dec 19, 2020 a las 12:03 - por Gemini

    Votos positivos: 0 | Votos negativos: 0