Control de concurrencia: bloqueo en dos fases y MVCC
Hay dos formas de que las transacciones no se pisen: bloquear lo que se va a tocar, o guardar varias versiones de cada fila. La segunda es la que usan casi todos los motores modernos, y explica por qué leer nunca espera.
Para este tema conviene tener claro:Transacciones y ACIDConcurrencia, semáforos y monitores
El aislamiento promete que las transacciones concurrentes den el mismo resultado que si corrieran una detrás de otra. Ejecutarlas realmente en serie sería correcto y también inaceptable. Los dos mecanismos que lo hacen viable son bloquear o versionar.
El bloqueo en dos fases, el enfoque pesimista
El enfoque pesimista es el bloqueo en dos fases. Cada transacción va tomando bloqueos sobre lo que lee y escribe —compartidos para leer, exclusivos para escribir— y, una vez que liberó el primero, no puede tomar ninguno nuevo.
Esa regla de dos fases —primero crecer, después soltar— es lo que garantiza la serializabilidad. En la práctica los bloqueos se liberan recién en el commit, para evitar que otra transacción lea algo que después se deshace.
Dos transacciones que van a tocar las mismas filas. Todavía no pasó nada.
Antes de seguir, predecí
Lo que cuesta bloquear
El costo es la espera. Un lector bloquea a los escritores y un escritor bloquea a los lectores, así que un reporte largo puede frenar la operación entera del sistema.
Además hay que bloquear rangos, no sólo filas, para evitar los fantasmas: si alguien consulta “los pedidos de hoy”, hay que impedir que otra transacción inserte uno nuevo en ese rango. Eso son bloqueos de rango o de brecha, y son una fuente frecuente de conflictos inesperados.
MVCC: una versión nueva en vez de sobrescribir
El enfoque optimista es MVCC: control de concurrencia multiversión. En vez de sobrescribir una fila, el motor crea una versión nueva y conserva la anterior, cada una marcada con la transacción que la creó.
Cada transacción ve una instantánea coherente: las versiones confirmadas antes de que ella empezara. De ahí sale la propiedad que cambia todo: leer no bloquea escribir y escribir no bloquea leer. El reporte largo ve la base como estaba cuando empezó y no frena a nadie.
MVCC concentra el conflicto en las escrituras
MVCC no elimina los conflictos, los concentra en las escrituras. Dos transacciones que modifican la misma fila siguen necesitando un bloqueo exclusivo, y una espera.
Y aparece un costo propio: las versiones viejas se acumulan y hay que limpiarlas. En PostgreSQL eso
es el VACUUM, y una transacción abierta durante horas impide limpiar todo lo posterior a ella, lo
que infla las tablas y degrada el rendimiento de a poco. El síntoma clásico —la base que se agranda
sola— suele ser una conexión olvidada con una transacción abierta.
Los interbloqueos aparecen igual
Con cualquiera de los dos mecanismos aparecen los interbloqueos: dos transacciones que se esperan mutuamente. El motor los detecta y cancela una, que es lo mejor que puede hacer.
Se reducen con una regla simple: acceder siempre a los recursos en el mismo orden. La mayoría de los interbloqueos reales son dos rutinas que tocan las mismas dos tablas en orden inverso. Y como no se pueden eliminar del todo, el código debe reintentar.
Leer, calcular y escribir con un número de versión
Para el caso de leer, calcular y escribir hay una alternativa sin bloqueos: el control optimista con
versión. La fila lleva un número, y la actualización incluye WHERE version = leida. Si actualizó
cero filas, alguien la cambió en el medio y hay que reintentar.
Es lo que hace un ORM cuando ofrece bloqueo optimista, y funciona muy bien cuando los conflictos son raros. Si son frecuentes, el reintento constante sale más caro que haber bloqueado desde el principio: la elección entre optimista y pesimista es una cuestión de tasa de conflicto.
Los niveles de aislamiento, con lo que dejan pasar
| Nivel | Lectura sucia | No repetible | Fantasma | Costo |
|---|---|---|---|---|
| Read uncommitted | sí | sí | sí | ninguno |
| Read committed | no | sí | sí | bajo, y es el default de muchos motores |
| Repeatable read | no | no | depende del motor | medio |
| Serializable | no | no | no | alto: espera o reintentos |
Cierre
Bloqueo en dos fases hace esperar y garantiza el orden; MVCC da instantáneas coherentes y logra que las lecturas nunca bloqueen, a cambio de acumular versiones que hay que limpiar. Los conflictos de escritura persisten en los dos, y con ellos los interbloqueos: mismo orden de acceso y reintento.
Autoevaluación
¿Lo entendiste?
Práctica