Esperar o reintentar
Coger turno o pedir perdón
En el episodio anterior, repeatable read salvaba la retirada de Luis abortándola al final: le dejaba avanzar y, cuando iba a escribir, le decía «lo que leíste ya no vale, vuelve a empezar». Hay otra forma de hacerlo: que el primero en llegar coja la cuenta y el segundo espere su turno.
Son dos filosofías con nombre. El bloqueo pesimista da por hecho que habrá conflicto y lo evita de entrada: bloquea la fila antes de leerla. El optimista da por hecho que no lo habrá: lee sin bloquear y, al escribir, comprueba que nadie ha cambiado nada; si alguien lo ha hecho, pide perdón y repite.
Ninguna es mejor en abstracto. Con dos cotitulares sacando dinero un sábado, da igual. Con veinte pagos por segundo en la cuenta de un comercio en Black Friday, no. Y hay una situación en la que las dos se quedan atascadas del todo: el deadlock que la serie de DDD dejó anunciado.
La misma cuenta, dos formas de protegerla
A la izquierda, cada cajero bloquea la fila al leerla (SELECT … FOR UPDATE) y la suelta al confirmar. A la derecha, lee el saldo y su número de versión, y al escribir solo actualiza si la versión sigue siendo la misma. Como en el episodio 2, «Paso de Ana» y «Paso de Luis» deciden el orden. En «mucha gente», la simulación cambia: cada barra es un pago en la cuenta de un comercio, y el tiempo corre hacia la derecha.
Qué ha pasado
Con poca gente, da igual
Ana y Luis sacan 100 y 200 € de la cuenta de gastos.probar en el laboratorio Con bloqueo pesimista, Ana lee con FOR UPDATE y se queda la fila; Luis intenta leer y espera. Cuando Ana confirma, Luis lee el saldo nuevo, 400 €, y todo cuadra. Con bloqueo optimista, los dos leen 500 € y la versión 0. Ana escribe con WHERE version = 0 y la sube a 1. Cuando Luis escribe con WHERE version = 0, su UPDATE afecta a 0 filas: alguien se ha adelantado. El cajero vuelve a leer y lo intenta otra vez.
Los dos llegan al mismo sitio. El pesimista paga en espera; el optimista, en trabajo repetido. Con dos personas, ninguno de los dos precios se nota.
Nada lento con la fila bloqueada
Ahora Ana saca 300 €, y el cajero consulta a Antifraudever en el mapa antes de escribir. Hoy Antifraude va lento.probar en el laboratorio Con bloqueo pesimista, Ana hace esa llamada con el FOR UPDATE tomado: mientras Antifraude piensa, nadie puede tocar la cuenta de gastos. Luis, que solo quería 50 €, espera por una llamada que no tiene nada que ver con él.
Es el fallo en cascada del episodio 1 de resiliencia, ahora dentro de la base de datos: un servicio lento no solo ocupa conexiones, también retiene bloqueos. Con bloqueo optimista, Luis ni se entera. La que paga es Ana: al volver de Antifraude, la versión ha cambiado y tiene que repetir la operación, llamada incluida. La lección vale para los dos modelos: ninguna llamada remota dentro de una transacción. Primero se pregunta a Antifraude, y luego se abre la transacción, corta.
El deadlock de dos transferencias cruzadas
Ana transfiere 50 € a Luis y, en el mismo instante, Luis transfiere 30 € a Ana.probar en el laboratorio Cada transferencia bloquea primero su cuenta de origen y luego la de destino. Ana tiene la suya y espera la de Luis; Luis tiene la suya y espera la de Ana. Ninguna puede seguir, y ninguna va a soltar lo que tiene. Es un deadlock.
La base de datos lo detecta: mira quién espera a quién, encuentra el ciclo y aborta una de las dos, que el cajero reintenta. No se pierde dinero, pero sí tiempo y un error que hay que saber reintentar. Y fíjate en el panel optimista: también se bloquea. Lee sin bloquear, pero al escribir las filas sí quedan bloqueadas hasta el COMMIT, y en el mismo orden cruzado.
El arreglo es el mismo en los dos: bloquear siempre en el mismo orden. Si las dos transferencias empiezan por la cuenta de IBAN menor, la segunda espera en la primera cuenta en lugar de quedarse con la otra.probar en el laboratorio Es lo que el ADR de la transferencia interna deja escrito como precio de hacerla en una sola transacción.
Veinte pagos por segundo
Cambia la escala. En Black Friday, a la cuenta de un comercio le llegan veinte pagos en un segundo, y cada uno tarda 120 ms en procesarse.probar en el laboratorio Con bloqueo pesimista se forma una cola: la fila solo la tiene uno a la vez, y los demás esperan su turno (la parte punteada de cada barra). Nadie repite nada, pero el último espera más de dos segundos.
Con bloqueo optimista nadie hace cola… y cada vez que uno confirma, todos los que estaban a medias ven la versión cambiada y tiran su trabajo (las barras rojas). Con veinte a la vez, eso son más de cien reintentos: la tormenta de reintentos del episodio 3 de resiliencia, esta vez dentro de una sola fila. Con tres pagos,probar en el laboratorio casi no hay diferencia.
Por qué compite todo el mundo por la misma fila es una decisión de diseño anterior: el tamaño del agregado. Eso lo contó el episodio 4 de DDD; aquí lo damos por hecho y miramos solo el mecanismo.
Criterio
| Pesimista | Optimista | |
|---|---|---|
| Cómo | SELECT … FOR UPDATE | Columna version y UPDATE … WHERE version = :v |
| Si hay conflicto | Espera | 0 filas: releer y repetir |
| Mejor con | Mucha contención sobre la misma fila; operaciones caras de repetir | Poca contención; lecturas largas o con el usuario pensando |
| Cuidado con | Retener el bloqueo mientras se espera algo lento; deadlocks | Tormentas de reintentos; repetir lo que no es idempotente |
-
¿Hay algo lento entre leer y escribir (una llamada, una pantalla que espera al usuario)?Un formulario que se edita durante minutos no puede tener la fila bloqueada.optimista, y nada lento dentro de la transacción
-
¿Muchos escriben a menudo la misma fila?La cuenta de un comercio, un contador de stock, el último asiento.pesimista (o partir la fila: episodio 4 de DDD)
-
¿Una transacción bloquea varias filas?Una transferencia entre dos cuentas.siempre en el mismo orden, y reintentar si hay deadlock
Y un último aviso para el optimista: reintentar solo es seguro si repetir no cambia nada. Si entre leer y escribir el cajero ya ha entregado los billetes, repetir es entregar dos veces. Es la idempotencia del episodio 4 de la serie de eventos, otra vez.
En el mundo real
SELECT … FOR UPDATE existe en PostgreSQL, MySQL, Oracle y SQL Server (allí con WITH (UPDLOCK)). Las variantes NOWAIT (fallar en vez de esperar) y SKIP LOCKED (saltarse las filas bloqueadas) son la base de muchas colas de trabajos hechas sobre una base de datos.
El bloqueo optimista con versión es lo habitual en los ORM: @Version en JPA/Hibernate, lock_version en Rails, concurrency tokens en Entity Framework. En DynamoDB se hace con escrituras condicionales; en Redis, con WATCH y MULTI; en HTTP, con las cabeceras ETag e If-Match, que devuelven 412 Precondition Failed si alguien se ha adelantado.
PostgreSQL busca deadlocks cuando una espera pasa de deadlock_timeout (1 s por defecto) y aborta con el error 40P01; MySQL/InnoDB los detecta al instante y aborta la transacción más pequeña. En los dos casos, la aplicación tiene que reintentar.
Siguiente episodio
Hasta ahora, los bloqueos solo aparecían al escribir. Pero ¿qué pasa con Reporting, que lee toda la tabla de cuentas para el informe regulatorio y tarda diez minutos? Si leer bloqueara, nadie podría pagar durante ese rato. Las bases de datos modernas no bloquean al leer. El truco tiene nombre, MVCC, y tiene letra pequeña.
Siguiente · Episodio 4 Cada uno con su foto MVCC: versiones de cada fila, lectores que no esperan a nadie y la transacción larga que no deja limpiar.