07 sept 2025·7 min de lectura

Bloqueos advisory de PostgreSQL para flujos seguros frente a concurrencia

Aprende los bloqueos advisory de PostgreSQL para evitar doble procesamiento en aprobaciones, facturación y schedulers con patrones prácticos, snippets SQL y comprobaciones sencillas.

Bloqueos advisory de PostgreSQL para flujos seguros frente a concurrencia

El problema real: dos procesos hacen el mismo trabajo

El doble procesamiento ocurre cuando el mismo elemento se gestiona dos veces porque dos actores distintos creen que son responsables. En aplicaciones reales se ve como un cliente cobrado dos veces, una aprobación aplicada dos veces o un correo de “factura lista” enviado en duplicado. Todo puede funcionar en pruebas y fallar bajo tráfico real.

Sucede cuando el timing se aprieta y más de una cosa puede actuar:

Dos workers cogen el mismo trabajo al mismo tiempo. Un reintento se dispara porque una llamada de red fue lenta, pero el primer intento sigue en ejecución. Un usuario hace doble clic en Aprobar porque la UI se congeló un segundo. Dos schedulers se solapan tras un deploy o por drift del reloj. Incluso un toque puede convertirse en dos peticiones si una app móvil reenvía tras un timeout.

La parte dolorosa es que cada actor se está comportando “razonablemente” por su cuenta. El bug es la brecha entre ellos: ninguno sabe que otro ya está procesando el mismo registro.

El objetivo es simple: para un elemento dado (una orden, una solicitud de aprobación, una factura), sólo un actor debe poder hacer el trabajo crítico a la vez. Los demás deberían esperar brevemente o retroceder y reintentar.

Los bloqueos advisory de PostgreSQL pueden ayudar. Te dan una forma ligera de decir “estoy trabajando en el elemento X” usando la base de datos que ya confías para la consistencia.

Ponte expectativas claras: un bloqueo no es un sistema de colas completo. No programará trabajos por ti, no garantiza orden y no almacena mensajes. Es una puerta de seguridad alrededor de la parte del flujo que nunca debe ejecutarse dos veces.

Qué son (y qué no son) los bloqueos advisory de PostgreSQL

Los bloqueos advisory de PostgreSQL son una manera de asegurar que solo un worker haga una pieza de trabajo a la vez. Eliges una clave de bloqueo (como “factura 123”), pides a la base de datos que la bloquee, haces el trabajo y luego la liberas.

La palabra “advisory” importa. Postgres no sabe qué significa tu clave y no protege nada automáticamente. Solo lleva la cuenta de un hecho: esta clave está bloqueada o no. Tu código debe acordar el formato de clave y debe tomar el bloqueo antes de ejecutar la parte riesgosa.

También conviene comparar los bloqueos advisory con los bloqueos de fila. Los bloqueos de fila (como SELECT ... FOR UPDATE) protegen filas reales de tablas y son excelentes cuando el trabajo se mapea limpiamente a una fila. Los advisory protegen una clave que eliges, lo cual es útil cuando el flujo toca muchas tablas, llama a servicios externos o empieza antes de que exista la fila.

Los advisory son útiles cuando necesitas:

  • Acciones una a la vez por entidad (una aprobación por solicitud, un cargo por factura)
  • Coordinación entre varios servidores de aplicación sin añadir un servicio de bloqueo aparte
  • Protección alrededor de un paso de workflow que es más grande que una sola actualización de fila

No son un reemplazo de otras herramientas de seguridad. No hacen que las operaciones sean idempotentes, no aplican reglas de negocio y no evitarán duplicados si un camino de código olvida tomar el bloqueo.

A menudo se les llama “ligeros” porque puedes usarlos sin cambios de esquema ni infraestructura extra. En muchos casos puedes arreglar el doble procesamiento añadiendo una llamada de bloqueo alrededor de una sección crítica mientras mantienes el resto del diseño igual.

Tipos de bloqueo que realmente usarás

Cuando la gente dice “bloqueos advisory de PostgreSQL” suelen referirse a un pequeño conjunto de funciones. Elegir la correcta cambia lo que pasa en errores, timeouts y reintentos.

Bloqueos de sesión vs transacción

Un bloqueo a nivel de sesión (pg_advisory_lock) dura mientras la conexión a la base de datos dure. Eso puede ser conveniente para workers de larga ejecución, pero también significa que un bloqueo puede quedarse si tu app se cae de forma que deja una conexión en el pool colgando.

Un bloqueo a nivel de transacción (pg_advisory_xact_lock) está ligado a la transacción actual. Al confirmar o deshacer, PostgreSQL lo libera automáticamente. Para la mayoría de workflows request-response (aprobaciones, clicks de facturación, acciones admin) este es el valor por defecto más seguro porque es fácil olvidar liberar un bloqueo de sesión.

Bloqueante vs try-lock

Las llamadas bloqueantes esperan hasta que el bloqueo esté disponible. Son simples, pero pueden hacer que una petición web se sienta atascada si otra sesión mantiene el bloqueo.

Las llamadas try-lock devuelven inmediatamente:

  • pg_try_advisory_lock (nivel sesión)
  • pg_try_advisory_xact_lock (nivel transacción)

Try-lock suele ser mejor para acciones de UI. Si el bloqueo está tomado, puedes devolver un mensaje claro como “Ya se está procesando” y pedir al usuario que reintente.

Compartido vs exclusivo

Los bloqueos exclusivos son “uno a la vez”. Los compartidos permiten múltiples poseedores pero bloquean a un exclusivo. La mayoría de problemas de doble procesamiento usan bloqueos exclusivos. Los compartidos son útiles cuando muchos lectores pueden proceder, pero un raro escritor debe ejecutarse solo.

Cómo se liberan los bloqueos

La liberación depende del tipo:

  • Bloqueos de sesión: se liberan al desconectar, o explícitamente con pg_advisory_unlock
  • Bloqueos de transacción: se liberan automáticamente cuando termina la transacción

Elegir la clave de bloqueo correcta

Un bloqueo advisory solo funciona si todos los workers intentan bloquear exactamente la misma clave para la misma pieza de trabajo. Si un camino de código bloquea “factura 123” y otro bloquea “cliente 45”, aún puedes tener duplicados.

Empieza por nombrar la “cosa” que quieres proteger. Hazla concreta: una factura, una solicitud de aprobación, una ejecución programada, o el ciclo de facturación mensual de un cliente. Esa elección decide cuánto concurrency permites.

Elige un alcance que coincida con el riesgo

La mayoría de equipos acaba con una de estas opciones:

  • Por registro: lo más seguro para aprobaciones y facturas (bloquear por invoice_id o request_id)
  • Por cliente/cuenta: útil cuando las acciones deben serializarse por cliente (facturación, cambios de crédito)
  • Por paso del workflow: cuando distintos pasos pueden correr en paralelo, pero cada paso debe ser uno a la vez

Trata el alcance como una decisión de producto, no sólo un detalle de base de datos. “Por registro” evita que un doble clic cobre dos veces. “Por cliente” evita que dos jobs en background generen estados solapados.

Elige una estrategia de clave estable

Normalmente tienes dos opciones: dos enteros de 32 bits (usados como espacio de nombres + id), o un entero de 64 bits (bigint), a veces creado al hashear un ID string.

Las claves de dos enteros son fáciles de estandarizar: escoge un número de namespace fijo por workflow (por ejemplo, aprobaciones vs facturación) y usa el ID del registro como segundo valor.

Hashear puede ser útil cuando tu identificador es un UUID, pero debes aceptar un pequeño riesgo de colisiones y ser consistente en todas partes.

Sea lo que sea, escríbelo y centralízalo. “Casi la misma clave” en dos sitios es una forma común de reintroducir duplicados.

Paso a paso: un patrón seguro para procesamiento uno a la vez

Gestiona el estado de bloqueo limpiamente
Devuelve mensajes rápidos como “ya en proceso” en lugar de colgar las peticiones.
Mejorar UX

Un buen workflow con advisory lock es simple: bloquear, verificar, actuar, registrar, confirmar. El bloqueo no es la regla de negocio por sí solo. Es una protección que hace que la regla sea fiable cuando dos workers golpean el mismo registro al mismo tiempo.

Un patrón práctico:

  1. Abre una transacción cuando el resultado deba ser atómico.
  2. Adquiere el bloqueo para la unidad específica de trabajo. Prefiere un bloqueo de transacción (pg_advisory_xact_lock) para que se libere automáticamente.
  3. Vuelve a comprobar el estado en la base de datos. No asumas que eres el primero. Confirma que el registro sigue elegible.
  4. Haz el trabajo y escribe un marcador “hecho” duradero en la base de datos (actualización de estado, entrada en ledger, fila de auditoría).
  5. Confirma y deja que el bloqueo se libere. Si usaste un bloqueo de sesión, haz pg_advisory_unlock antes de devolver la conexión al pool.

Ejemplo: dos servidores de app reciben “Aprobar factura #123” en el mismo segundo. Ambos empiezan, pero solo uno obtiene el bloqueo para 123. El ganador comprueba que la factura #123 sigue pending, la marca approved, escribe la entrada de auditoría/pago y confirma. El segundo servidor o bien falla rápido (try-lock) o espera y luego, al reintentar, ve que ya está aprobada y sale sin duplicar nada. De cualquier forma, evitas doble procesamiento y la UI sigue respondiendo.

Para depurar, registra suficiente información para rastrear cada intento: id de petición, id de aprobación y clave de bloqueo calculada, id del actor, resultado (lock_busy, already_approved, approved_ok) y tiempos.

Dónde encajan los advisory locks: aprobaciones, facturación, schedulers

Los advisory locks encajan mejor cuando la regla es sencilla: para una cosa específica, sólo un proceso puede hacer el trabajo “ganador” a la vez. Mantienes tu base de datos y código existentes, pero añades una pequeña puerta que hace que las condiciones de carrera sean mucho más difíciles de provocar.

Aprobaciones

Las aprobaciones son trampas clásicas de concurrencia. Dos revisores (o la misma persona haciendo doble clic) pueden pulsar Aprobar en milisegundos. Con un bloqueo por el id de la solicitud, solo una transacción realiza el cambio de estado. Los demás aprenden rápidamente el resultado y pueden mostrar un mensaje claro como “ya aprobado” o “ya rechazado”.

Esto es común en portales de clientes y paneles admin donde mucha gente vigila la misma cola.

Facturación

La facturación suele necesitar una regla estricta: un intento de pago por factura, incluso cuando hay reintentos. Un timeout de red puede hacer que un usuario pulse Pagar otra vez, o un reintento en background puede ejecutarse mientras el primer intento sigue en vuelo.

Un bloqueo por invoice ID asegura que solo un camino hable con el proveedor de pagos a la vez. El segundo intento puede devolver “pago en progreso” o leer el estado de pago más reciente. Eso previene trabajo duplicado y reduce el riesgo de cargos dobles.

Schedulers y workers en background

En setups multi-instancia, los schedulers pueden ejecutar la misma ventana en paralelo por accidente. Un bloqueo por nombre de job más ventana temporal (por ejemplo, “daily-settlement:2026-01-29”) asegura que solo una instancia lo ejecute.

El mismo enfoque funciona para workers que extraen items de una tabla: bloquea por ID del ítem para que solo un worker lo procese.

Claves comunes incluyen un ID de solicitud de aprobación, un ID de factura, un nombre de job más ventana temporal, un ID de cliente para “una exportación a la vez” o una clave de idempotencia única para reintentos.

Un ejemplo realista: evitar la doble aprobación en un portal

Estandariza tus claves de bloqueo
Crea un helper compartido de claves de bloqueo en tu backend para que cada flujo use la misma clave.
Construir backend

Imagina una solicitud de aprobación en un portal: una orden de compra está esperando y dos managers hacen clic en Aprobar en el mismo segundo. Sin protección, ambas peticiones pueden leer “pending” y ambas escribir “approved”, creando entradas de auditoría duplicadas, notificaciones repetidas o trabajo downstream disparado dos veces.

Los advisory locks de PostgreSQL te dan una forma directa de hacer que esta acción sea una a la vez por aprobación.

El flujo

Cuando la API recibe una acción de aprobar, primero toma un bloqueo basado en el id de aprobación (así distintas aprobaciones pueden procesarse en paralelo).

Un patrón común es: bloquear por approval_id, leer el estado actual, actualizar el estado y escribir una fila de auditoría, todo en una transacción.

BEGIN;

-- One-at-a-time per approval_id
SELECT pg_try_advisory_xact_lock($1) AS got_lock;  -- $1 = approval_id

-- If got_lock = false, return "someone else is approving, try again".

SELECT status FROM approvals WHERE id = $1 FOR UPDATE;

-- If status != 'pending', return "already processed".

UPDATE approvals
SET status = 'approved', approved_by = $2, approved_at = now()
WHERE id = $1;

INSERT INTO approval_audit(approval_id, actor_id, action, created_at)
VALUES ($1, $2, 'approved', now());

COMMIT;

Qué experimenta el segundo clic

La segunda petición o no consigue el bloqueo (y entonces devuelve rápido “En proceso”) o consigue el bloqueo tras el primer commit, ve que el estado ya está aprobado y sale sin cambiar nada. En cualquiera de los casos evitas el doble procesamiento y mantienes la UI responsable.

Para depuración, registra id de petición, id de aprobación y clave, id del actor, resultado (lock_busy, already_approved, approved_ok) y tiempos.

Manejar esperas, timeouts y reintentos sin congelar la app

Arregla una acción crítica primero
Elige cobrar una factura o aprobar una solicitud y protégela con un bloqueo transaccional.
Comenzar

Esperar un bloqueo suena inofensivo hasta que se convierte en un botón que gira, un worker atascado o una cola que nunca se despeja. Cuando no puedas obtener el bloqueo, falla rápido donde hay un humano esperando y espera solo donde la espera sea segura.

Para acciones de usuario: try-lock y responder claro

Si alguien pulsa Aprobar o Cobrar, no bloquees su petición durante segundos. Usa try-lock para que la app pueda responder al instante.

Un enfoque práctico: intenta bloquear y, si falla, devuelve una respuesta clara de “ocupado, inténtalo de nuevo” (o refresca el estado del ítem). Eso reduce timeouts y desalienta clics repetidos.

Mantén la sección bloqueada corta: valida estado, aplica el cambio y confirma.

Para jobs en background: bloquear está bien, pero pon un tope

Para schedulers y workers, bloquear puede estar bien porque no hay un humano esperando. Pero necesitas límites para que un job lento no pare una flota entera.

Usa timeouts para que un worker pueda rendirse y pasar al siguiente:

SET lock_timeout = '2s';
SET statement_timeout = '30s';
SELECT pg_advisory_lock(123456);

También define un tiempo máximo esperado para el job en sí. Si la facturación normalmente termina en menos de 10 segundos, trata 2 minutos como incidente. Registra tiempo de inicio, id del job y cuánto tiempo se mantienen los bloqueos. Si tu runner soporta cancelación, cancela tareas que superen el tope para que la sesión termine y el bloqueo se libere.

Planifica reintentos a propósito. Cuando no se adquiere un bloqueo, decide qué sucede: reprogramar pronto con backoff (y algo de aleatoriedad), omitir trabajo best-effort en este ciclo o marcar el ítem como contendido si fallos repetidos requieren atención.

Errores comunes que causan bloqueos atascados o duplicados

La sorpresa más común son los bloqueos de sesión que nunca se liberan. Los pools de conexión mantienen conexiones abiertas, así que una sesión puede sobrevivir a la petición. Si tomas un bloqueo de sesión y olvidas desbloquear, el bloqueo puede quedarse hasta que la conexión se recicle. Otros workers esperarán (o fallarán) y puede ser difícil saber por qué.

Otra fuente de duplicados es bloquear pero no comprobar el estado. Un bloqueo solo asegura que un worker ejecute la sección crítica a la vez. No garantiza que el registro siga siendo elegible. Siempre vuelve a comprobar dentro de la misma transacción (por ejemplo, confirma pending antes de pasar a approved).

Las claves de bloqueo también suelen confundir a los equipos. Si un servicio bloquea por order_id y otro por una clave calculada diferente para el mismo recurso real, ahora tienes dos bloqueos. Ambos caminos pueden ejecutarse a la vez, lo que crea una falsa sensación de seguridad.

Mantener bloqueos mucho tiempo suele ser autoinfligido. Si haces llamadas lentas en red mientras mantienes el bloqueo (proveedor de pagos, email/SMS, webhooks), una valla corta se convierte en cuello de botella. Mantén la sección bloqueada enfocada en trabajo rápido de base de datos: validar estado, escribir el nuevo estado y registrar lo que debe pasar después. Luego lanza los efectos secundarios tras el commit.

Finalmente, los advisory locks no reemplazan la idempotencia ni las constraints de base de datos. Trátalos como un semáforo, no como un sistema de prueba. Usa restricciones únicas donde correspondan y claves de idempotencia para llamadas externas.

Lista rápida de comprobaciones antes de lanzar

Haz seguras las reintentos de facturación
Añade procesamiento de facturas uno a la vez antes de llamar a Stripe u otros pagos.
Comenzar a crear

Trata los advisory locks como un pequeño contrato: todo el equipo debe saber qué significa el bloqueo, qué protege y qué está permitido mientras está activo.

Una lista corta que evita la mayoría de problemas:

  • Una clave de bloqueo clara por recurso, documentada y reutilizada en todas partes
  • Adquirir el bloqueo antes de algo irreversible (pagos, correos, llamadas a APIs externas)
  • Volver a comprobar el estado después de adquirir el bloqueo y antes de escribir cambios
  • Mantener la sección bloqueada corta y medible (registra espera de bloqueo y tiempo de ejecución)
  • Decidir qué significa “lock busy” para cada path (mensaje UI, reintento con backoff, omitir)

Siguientes pasos: aplica el patrón y mantenlo sostenible

Elige un lugar donde los duplicados te hagan más daño y empieza ahí. Buenos primeros objetivos son acciones que cuestan dinero o cambian estado permanentemente, como “cobrar factura” o “aprobar solicitud”. Envuelve sólo esa sección crítica con un bloqueo advisory y luego expande a pasos cercanos cuando confíes en el comportamiento.

Añade observabilidad básica desde el principio. Registra cuando un worker no puede obtener un bloqueo y cuánto tardan los trabajos bloqueados. Si las esperas de bloqueo suben, suele significar que la sección crítica es demasiado grande o hay una consulta lenta escondida.

Los bloqueos funcionan mejor sobre la seguridad de los datos, no en lugar de ella. Mantén campos de estado claros (pending, processing, done, failed) y respáldalos con constraints donde puedas. Si un reintento llega en el peor momento, una constraint única o una clave de idempotencia puede ser la segunda línea de defensa.

Si estás construyendo workflows en AppMaster (appmaster.io), puedes aplicar el mismo patrón manteniendo el cambio de estado crítico dentro de una transacción y añadiendo un pequeño paso SQL para tomar un bloqueo advisory a nivel de transacción antes del paso de “finalizar”.

Los advisory locks encajan hasta que realmente necesites características de colas (prioridades, trabajos retrasados, manejo de dead-letter), tengas alta contención y necesites paralelismo más inteligente, debas coordinar entre bases de datos sin un Postgres compartido, o necesites reglas de aislamiento más estrictas. La meta es fiabilidad aburrida: mantiene el patrón pequeño, consistente, visible en logs y respaldado por constraints.

FAQ

¿Cuándo debo usar bloqueos advisory de PostgreSQL en lugar de confiar sólo en la lógica de mi aplicación?

Usa un bloqueo advisory cuando necesitas “sólo un actor a la vez” para una unidad específica de trabajo, como aprobar una solicitud, cobrar una factura o ejecutar una ventana programada. Es especialmente útil cuando múltiples instancias de la app pueden tocar el mismo elemento y no quieres añadir un servicio de bloqueo separado.

¿En qué se diferencian los bloqueos advisory de `SELECT ... FOR UPDATE`?

Los bloqueos de fila protegen filas reales que seleccionas y son ideales cuando toda la operación se mapea limpiamente a una actualización de una sola fila. Los bloqueos advisory protegen una clave que eliges, por lo que funcionan incluso cuando el flujo toca muchas tablas, llama a servicios externos o empieza antes de que exista la fila final.

¿Debo usar bloqueos a nivel de transacción o de sesión?

Por defecto, utiliza pg_advisory_xact_lock (nivel transacción) para acciones request/response porque se libera automáticamente al confirmar o deshacer. Usa pg_advisory_lock (nivel sesión) solo cuando realmente necesites que el bloqueo sobreviva a la transacción y estés seguro de llamar a pg_advisory_unlock antes de devolver la conexión al pool.

¿Es mejor bloquear esperando el bloqueo o usar try-lock?

Para acciones impulsadas por la UI, prefiere try-lock (pg_try_advisory_xact_lock) para que la petición pueda fallar rápido y devolver una respuesta clara de “ya en proceso”. Para workers en background, un bloqueo que espere puede estar bien, pero ponle un lock_timeout para que una tarea atascada no bloquee todo.

¿Debo bloquear por ID de registro, ID de cliente u otra cosa?

Bloquea lo más pequeño que no debe ejecutarse dos veces, normalmente “una factura” o “una solicitud de aprobación”. Si bloqueas demasiado ampliamente (por ejemplo, por cliente) puedes reducir el rendimiento; si bloqueas demasiado estrechamente puedes seguir teniendo duplicados.

¿Cómo elijo una clave de bloqueo para que todos los servicios usen exactamente la misma?

Elige un formato de clave estable y úsalo en todos los sitios que puedan realizar la misma acción crítica. Un enfoque común es usar dos enteros: un namespace fijo para el flujo y el ID de la entidad, así distintos flujos no se bloquean entre sí por accidente pero aún se coordinan correctamente.

¿Los bloqueos advisory reemplazan las comprobaciones de idempotencia o las restricciones únicas?

No. Un bloqueo solo evita la ejecución concurrente; no demuestra que la operación sea segura de repetir. Aún debes volver a comprobar el estado dentro de la transacción (por ejemplo, verificar que el ítem sigue pending) y confiar en constraints únicas o claves de idempotencia donde apliquen.

¿Qué debo hacer dentro de la sección bloqueada para evitar ralentizar todo?

Mantén la sección bloqueada corta y centrada en la base de datos: adquiere el bloqueo, vuelve a comprobar la elegibilidad, escribe el nuevo estado y confirma. Realiza efectos secundarios lentos (pagos, correos, webhooks) después del commit o mediante un registro de outbox para no mantener el bloqueo durante retrasos en la red.

¿Por qué a veces los bloqueos advisory parecen “atascados” incluso después de que la petición terminó?

La causa más común es un bloqueo a nivel de sesión mantenido por una conexión en pool que nunca fue desbloqueada por un bug en el código. Prefiere bloqueos a nivel de transacción y, si debes usar sesión, asegúrate de que pg_advisory_unlock se ejecute de forma fiable antes de devolver la conexión al pool.

¿Qué debo registrar o monitorizar para saber si los bloqueos advisory están funcionando?

Registra el ID de la entidad y la clave de bloqueo calculada, si se adquirió el bloqueo, cuánto tardó en adquirirse y cuánto duró la transacción. También registra el resultado como lock_busy, already_processed o processed_ok para distinguir contención de duplicados reales.

Fácil de empezar
Crea algo sorprendente

Experimente con AppMaster con plan gratuito.
Cuando esté listo, puede elegir la suscripción adecuada.

Empieza