Episodio 4

Cada uno con su foto

lectura · 12 mindominio · BCPS Bankoperaciones · informe regulatorio, pagos con tarjeta

El informe de las tres de la mañana

Cada noche, Reportingver en el mapa suma el dinero de todas las cuentas de BCPS Bank para un informe regulatorio. Tarda diez minutos, y durante esos diez minutos la gente sigue pagando: gasolineras, farmacias de guardia, compras por internet. El informe tiene que cuadrar al céntimo, así que no puede sumar la mitad de las cuentas antes de un pago y la otra mitad después.

La solución de toda la vida es bloquear: lo que el informe ha leído no se toca hasta que termine. Funciona, y deja a la gasolinera esperando diez minutos. La solución de casi todas las bases de datos modernas es otra: que el informe lea una foto del momento en que empezó, mientras los pagos siguen cambiando la tabla por debajo. Es lo que el episodio 2 llamaba snapshot. Hoy vemos cómo se hace.

Leer sin que nadie espere

Reporting lee tres cuentas, una detrás de otra, en una transacción. Entre medias llegan pagos con tarjeta. A la izquierda, leer bloquea la fila hasta el final, y cada fila tiene un solo valor. A la derecha, MVCC: cada pago crea una versión nueva de la fila, y la tarjeta «Las filas por dentro» las enseña todas. Da los pasos tú, o usa el orden del escenario, y cuando el informe termine, pulsa VACUUM.

Qué ha pasado

Con bloqueos: el informe hace esperar a los pagos

En el panel de la izquierda, leer coge un bloqueo compartido sobre la fila y no lo suelta hasta el COMMIT. Otros pueden leerla, pero nadie puede escribirla.probar en el laboratorio El primer pago de la cuenta de gastos llega justo después de que el informe la haya leído, y se queda esperando; los que vienen detrás, también. Cuando el informe confirma, la cola se vacía de golpe.

Así funcionaban las bases de datos con bloqueo en dos fases, y así sigue funcionando SQL Server en serializable si no se activa el snapshot. Es correcto: el informe cuadra y ningún pago se pierde. Es lento para todos los demás.

Con MVCC: una versión por cambio

En el panel de la derecha, un pago no sobrescribe la fila: crea una versión nueva y marca la anterior como sustituida, apuntando qué transacción creó cada una y cuál la dejó vieja. Es el control de concurrencia multiversión, MVCC.

Cada transacción lee la versión que era la buena cuando se hizo su foto. El informe empezó antes de los pagos, así que sigue viendo 500 € en la cuenta de gastos aunque ya haya dos versiones más nuevas, y su total es el de un instante concreto: cuadra. Los pagos no esperan al informe, y el informe no espera a los pagos. Los lectores no bloquean a los escritores, ni los escritores a los lectores.

Este es el repeatable read del episodio 2 visto por dentro: la foto de la transacción es la regla que decide qué versión ve. Y read committed es lo mismo con una foto nueva en cada sentencia.

MVCC no quita todas las esperas

Si dos transacciones quieren escribir la misma fila, siguen esperando.probar en el laboratorio Solo puede haber una versión «siguiente», así que la segunda espera a que la primera confirme o se deshaga. Es el bloqueo de fila del episodio 3, y MVCC no lo toca: separa a lectores de escritores, no a escritores entre sí.

El precio: las versiones viejas

Cada pago ha dejado una versión vieja. Mientras el informe siga abierto, no se pueden borrar, porque su foto aún podría necesitarlas: en la tarjeta aparecen en naranja. Cuando el informe confirma, ya nadie las ve, y VACUUM, el proceso que las recoge, las puede borrar. Pulsa V antes y después de que el informe termine y compara.

Ahora imagina que el informe no termina nunca.probar en el laboratorio Alguien abre una transacción en una consola, lanza la consulta y se va a comer. La sesión se queda idle in transaction, con su foto vieja. A la izquierda, los pagos de las cuentas que ha leído no pasan nunca. A la derecha pasan todos, pero cada uno deja una versión que VACUUM no puede tocar, y la tabla crece y crece. En producción, eso es una tabla de cuentas que pasa de 2 GB a 40 en una tarde, y consultas cada vez más lentas porque tienen que saltar versiones muertas.

Criterio

Quién espera a quiénBloqueosMVCC
Un lector a un escritorSíNo: lee la versión de su foto
Un escritor a un lectorSíNo
Un escritor a otro escritorSíSí
PrecioEsperas; deadlocks entre lectores y escritoresEspacio: versiones viejas hasta que VACUUM las borra

No es una elección que se haga a menudo: viene con la base de datos. Lo que sí se decide es cómo convivir con ella:

En el mundo real

Casi todas las bases de datos relacionales actuales usan MVCC, pero de dos formas. PostgreSQL guarda las versiones en la propia tabla (cada fila lleva xmin y xmax, las transacciones que la crearon y la borraron), y por eso necesita VACUUM y su versión automática, autovacuum. Oracle y MySQL/InnoDB actualizan la fila en su sitio y guardan lo necesario para reconstruir la versión vieja en un área aparte (undo); una transacción larga ahí hace crecer el undo, y en Oracle puede acabar en el famoso error ORA-01555: snapshot too old.

SQL Server usa bloqueos por defecto y ofrece MVCC como opción (READ_COMMITTED_SNAPSHOT, SNAPSHOT), con las versiones en tempdb. CockroachDB, TiDB y otras distribuidas usan MVCC con marcas de tiempo, y recogen la basura pasado un plazo configurable.

En PostgreSQL, las transacciones que retienen versiones se ven en pg_stat_activity (columna backend_xmin), y las filas muertas de cada tabla, en pg_stat_user_tables. Son las dos primeras consultas cuando una tabla crece sin motivo.

Siguiente episodio

Hasta aquí, todo ha pasado dentro de una sola base de datos, que siempre sabe quién tiene qué. Ahora Ana y Luis se van de fin de semana a un pueblo, y el único cajero pierde la conexión con el banco. ¿Deja sacar dinero o no? No hay una respuesta correcta: hay una decisión de negocio, y un teorema que dice que no se pueden tener las dos cosas.

Siguiente · Episodio 5 El cajero sin conexión CAP y PACELC: disponibilidad o consistencia cuando se corta la red, y latencia o consistencia cuando no.

← Todos los episodios · Reporting en el mapa de BCPS Bank