Cómo las bases de datos gestionan las transacciones simultáneas sin corromper los datos
El control de concurrencia es el conjunto de mecanismos que un sistema de gestión de bases de datos (DBMS) utiliza para gestionar transacciones simultáneas sin corromper los datos. Cuando varios usuarios o procesos leen y escriben al mismo tiempo, el acceso no controlado conduce a anomalías en los datos, como actualizaciones perdidas, lecturas sucias y resultados inconsistentes que pueden propagarse a través de sistemas posteriores.
El control de concurrencia aplica la "I" (Aislamiento) de las transacciones ACID: las transacciones concurrentes deben producir el mismo resultado que si se ejecutaran una tras otra, una propiedad conocida como serializabilidad. Sin él, una aplicación bancaria podría perder transferencias, un sistema de inventario podría vender productos en exceso y un pipeline de análisis podría producir informes basados en datos a medio escribir.
Esta guía cubre los problemas centrales de concurrencia, los principales mecanismos para resolverlos —bloqueo, control de concurrencia multiversión (MVCC) y estrategias optimistas y pesimistas— junto con los niveles de aislamiento, el manejo de interbloqueos y la orientación práctica para elegir el enfoque correcto para su carga de trabajo.
Los sistemas de bases de datos modernos manejan rutinariamente miles a millones de transacciones concurrentes por segundo. Cada vez que un usuario envía una consulta, actualiza un registro o inicia un pipeline de ETL, el DBMS debe coordinar esa operación junto con todas las demás operaciones que ocurren en el mismo momento. Sin control de concurrencia, las transacciones intercaladas producen resultados impredecibles e incorrectos.
Considere un escenario concreto: dos usuarios actualizan el saldo de la misma cuenta bancaria simultáneamente. El usuario A lee el saldo (1.000 $), resta 200 $ y escribe 800 $. El usuario B también lee 1.000 $, suma 500 $ y escribe 1.500 $. El resultado correcto depende del orden —ya sea 800 $ o 1.500 $— pero sin coordinación, una escritura simplemente sobrescribe la otra. Esta es una actualización perdida clásica, una de varias anomalías que el control de concurrencia existe para prevenir.
El control de concurrencia impone la serializabilidad: el resultado de la ejecución concurrente debe coincidir con algún orden serial de esas mismas transacciones. Esto apoya directamente las cuatro garantías ACID: atomicidad (todo o nada), consistencia (transiciones de estado válidas), aislamiento (sin interferencia) y durabilidad (los cambios confirmados son permanentes). Esto garantiza la base sobre la cual cada aplicación construye la confianza en sus datos.
Antes de explorar las soluciones, es útil comprender las anomalías específicas que surgen cuando las transacciones concurrentes se ejecutan sin los controles adecuados.
Estas cuatro anomalías son el "por qué" detrás de cada mecanismo de control de concurrencia. Cada mecanismo previene algunas o todas ellas, dependiendo del nivel de aislamiento que impone.
El bloqueo es el enfoque más antiguo e intuitivo para el control de concurrencia. El DBMS asigna bloqueos a elementos de datos —filas, páginas o tablas enteras— antes de permitir que las transacciones accedan a ellos. Un bloqueo actúa como un portero: si otra transacción ya tiene un bloqueo conflictivo, la transacción solicitante debe esperar.
La granularidad del bloqueo introduce una compensación importante. Los bloqueos a nivel de fila maximizan la concurrencia porque las filas no relacionadas permanecen accesibles, pero agregan sobrecarga porque el sistema debe rastrear muchos bloqueos individuales. Los bloqueos a nivel de tabla son más simples y menos costosos de administrar, pero obligan a las transacciones no relacionadas a esperar, lo que reduce el rendimiento.
El bloqueo de dos fases es el protocolo estándar para garantizar la serializabilidad a través de bloqueos. Divide cada transacción en dos fases. Durante la fase de crecimiento, una transacción adquiere todos los bloqueos que necesita pero nunca libera ninguno. Una vez que libera su primer bloqueo, entra en la fase de encogimiento y solo puede liberar bloqueos, nunca adquirir nuevos. Esta regla garantiza que el orden en que las transacciones bloquean los elementos de datos sea consistente, lo que a su vez garantiza un programa serializable.
Strict 2PL es una variante común que mantiene todos los bloqueos hasta que la transacción se confirma. Esto previene reversiones en cascada, una situación en la que una transacción abortada obliga a otras transacciones que leyeron sus datos no confirmados a abortar también. La mayoría de las bases de datos relacionales que utilizan control de concurrencia basado en bloqueos implementan alguna forma de strict 2PL.
La desventaja de 2PL es que introduce bloqueos —las transacciones deben esperar por los bloqueos que tienen otras— y crea las condiciones para los interbloqueos.
MVCC adopta un enfoque fundamentalmente diferente: en lugar de bloquear el acceso, mantiene múltiples versiones de cada elemento de datos para que cada transacción vea una instantánea coherente de la base de datos. Cuando una transacción escribe una fila, MVCC crea una nueva versión en lugar de sobrescribir la existente. Los lectores continúan viendo la versión que estaba vigente cuando comenzó su transacción.
La principal ventaja es que los lectores nunca bloquean a los escritores y los escritores nunca bloquean a los lectores. Este es un beneficio de rendimiento importante para cargas de trabajo con muchas lecturas, razón por la cual MVCC es el mecanismo de control de concurrencia dominante en las bases de datos modernas. PostgreSQL, MySQL/InnoDB, Oracle y la mayoría de los sistemas contemporáneos de OLTP y OLAP utilizan alguna forma de MVCC.
Los conflictos de escritura se detectan en el momento de la confirmación. Si dos transacciones modifican la misma fila, la primera en confirmarse gana y la segunda debe reintentar. Este enfoque optimista para la resolución de conflictos de escritura mantiene un alto rendimiento cuando los conflictos son raros, como suele ser el caso.
Aislamiento de instantáneas es el nivel de aislamiento más común basado en MVCC. Cada transacción ve la base de datos como estaba cuando comenzó la transacción, lo que previene lecturas sucias y lecturas no repetibles. Sin embargo, el aislamiento de instantáneas permite una anomalía sutil llamada sesgo de escritura, donde dos transacciones leen datos superpuestos y luego realizan escrituras basadas en lo que leyeron, lo que resulta en un estado que ninguna de las transacciones habría producido por sí sola. El aislamiento de instantáneas serializable (SSI) aborda esta brecha a costa de una sobrecarga adicional.
MVCC introduce sus propias compensaciones. Mantener múltiples versiones consume almacenamiento adicional y el sistema debe recolectar periódicamente versiones obsoletas, un proceso conocido como VACUUM en PostgreSQL. Bajo una alta contención de escritura, el costo de reintento y la sobrecarga de gestión de versiones pueden volverse significativos.
Más allá de los mecanismos específicos de bloqueo y MVCC, las estrategias de control de concurrencia se dividen en dos campos filosóficos: pesimista y optimista. Comprender qué campo se adapta a su carga de trabajo es una de las decisiones arquitectónicas más importantes que puede tomar un equipo de datos.
El control de concurrencia pesimista asume que los conflictos son probables y los previene adquiriendo bloqueos antes de acceder a los datos. Este enfoque es mejor para cargas de trabajo con muchas escrituras donde los conflictos son frecuentes y el costo de revertir y reintentar una transacción es alto. Los sistemas de libros de contabilidad financieros, donde cada escritura debe secuenciarse y verificarse, son un caso de uso clásico.
La desventaja es el bloqueo. Bajo alta concurrencia, las transacciones se ponen en cola esperando bloqueos, lo que reduce el rendimiento. En el peor de los casos, las solicitudes de bloqueo en competencia crean interbloqueos.
El control de concurrencia optimista asume que los conflictos son raros. Las transacciones se ejecutan libremente sin adquirir bloqueos, operando en sus propias copias privadas de los datos. En el momento de la confirmación, el sistema valida si las lecturas y escrituras de la transacción entran en conflicto con alguna otra transacción confirmada. Si no se detectan conflictos, la transacción se confirma. Si se detecta un conflicto, la transacción se revierte y se reintenta.
Este modelo de tres fases — leer, validar, escribir — sobresale en entornos donde la mayoría de las transacciones no tocan los mismos datos. Los sistemas de gestión de contenido, las aplicaciones de navegación de catálogos y los paneles de informes son ejemplos típicos.
La desventaja es el trabajo desperdiciado. Cuando los conflictos son frecuentes, las transacciones ejecutan repetidamente su lógica completa solo para ser revertidas en la validación, consumiendo recursos sin producir resultados.
La elección entre control pesimista y optimista depende de las características de la carga de trabajo. La alta contención de escritura, las transacciones cortas y la baja tolerancia a reintentos apuntan hacia el control pesimista. Las cargas de trabajo de lectura intensiva con baja probabilidad de conflicto y altos requisitos de concurrencia favorecen los enfoques optimistas o basados en MVCC.
En la práctica, muchos sistemas de producción combinan ambas estrategias. Un patrón común utiliza MVCC para lecturas y escrituras generales, mientras aplica bloqueos pesimistas en filas de puntos calientes conocidos; por ejemplo, usando SELECT ... FOR UPDATE para bloquear una fila específica que múltiples transacciones probablemente contendrán simultáneamente.