ACTUALIZACIÓN de Postgres ... LÍMITE 1


77

Tengo una base de datos Postgres que contiene detalles sobre grupos de servidores, como el estado del servidor ('activo', 'en espera', etc.). Los servidores activos en cualquier momento pueden necesitar conmutar por error a un modo de espera, y no me importa qué modo de espera se use en particular.

Quiero una consulta de base de datos para cambiar el estado de un modo de espera, SOLO UNO, y devolver la IP del servidor que se utilizará. La elección puede ser arbitraria: dado que el estado del servidor cambia con la consulta, no importa qué modo de espera esté seleccionado.

¿Es posible limitar mi consulta a una sola actualización?

Esto es lo que tengo hasta ahora:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

A Postgres no le gusta esto. ¿Qué podría hacer de manera diferente?


Simplemente elija el servidor en código y agréguelo como restringido. Esto también le permite tener condiciones adicionales (las más antiguas, las más recientes, las más recientes vivas, las menos cargadas, la misma CC, el estante diferente, la menor cantidad de errores) verificadas primero de todos modos. La mayoría de los protocolos de conmutación por error requieren alguna forma de determinismo de todos modos.
Eckes

@eckes Esa es una idea interesante. En mi caso, "elegir el servidor en código" habría significado leer primero una lista de servidores disponibles de la base de datos y luego actualizar un registro. Debido a que muchas instancias de la aplicación podrían realizar esta acción, hay una condición de carrera y se necesita una operación atómica (o fue hace 5 años). La elección no necesitaba ser determinista.
vastlysuperiorman

Respuestas:


125

Sin acceso de escritura concurrente

Materialice una selección en un CTE y únase a ella en la FROMcláusula del UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

Originalmente tenía una subconsulta simple aquí, pero eso puede eludir LIMITciertos planes de consulta como señaló Feike :

El planificador puede elegir generar un plan que ejecute un bucle anidado sobre la LIMITingsubconsulta, causando más UPDATEsde LIMIT, por ejemplo:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Reproducir caso de prueba

La forma de arreglar lo anterior fue envolver la LIMITsubconsulta en su propio CTE, ya que el CTE se materializa no devolverá resultados diferentes en diferentes iteraciones del bucle anidado.

O utilice una subconsulta poco correlacionada para el caso simple conLIMIT 1. Más simple, más rápido:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

Con acceso de escritura concurrente

Asumiendo el nivel de aislamiento predeterminadoREAD COMMITTED para todo esto. Los niveles de aislamiento más estrictos ( REPEATABLE READy SERIALIZABLE) aún pueden provocar errores de serialización. Ver:

Bajo carga de escritura concurrente, agregue FOR UPDATE SKIP LOCKEDpara bloquear la fila para evitar condiciones de carrera. SKIP LOCKEDfue agregado en Postgres 9.5 , para versiones anteriores ver abajo. El manual:

Con SKIP LOCKED, se omiten las filas seleccionadas que no se pueden bloquear inmediatamente. Omitir filas bloqueadas proporciona una vista inconsistente de los datos, por lo que esto no es adecuado para el trabajo de propósito general, pero puede usarse para evitar la contención de bloqueos con múltiples consumidores que acceden a una tabla tipo cola.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

Si no queda una fila desbloqueada que califique, no sucede nada en esta consulta (no se actualiza ninguna fila) y obtiene un resultado vacío. Para operaciones no críticas, eso significa que has terminado

Sin embargo, las transacciones concurrentes pueden tener filas bloqueadas, pero luego no terminan la actualización ( ROLLBACKu otras razones). Para estar seguro, ejecute una verificación final:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTTambién ve filas bloqueadas. Si bien eso no regresa true, una o más filas aún se están procesando y las transacciones aún podrían revertirse. (O mientras tanto, se han agregado nuevas filas). Espere un poco, luego repita los dos pasos: ( UPDATEhasta que no recupere la fila; SELECT...) hasta que llegue true.

Relacionado:

Sin SKIP LOCKEDen PostgreSQL 9.4 o anterior

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Las transacciones concurrentes que intentan bloquear la misma fila se bloquean hasta que la primera libera su bloqueo.

Si se revierte la primera, la siguiente transacción toma el bloqueo y continúa normalmente; otros en la cola siguen esperando.

Si se confirma por primera vez, la WHEREcondición se vuelve a evaluar y si ya no TRUEexiste ( statusha cambiado) el CTE (algo sorprendente) no devuelve ninguna fila. No pasa nada. Ese es el comportamiento deseado cuando todas las transacciones desean actualizar la misma fila .
Pero no cuando cada transacción quiere actualizar la siguiente fila . Y dado que solo queremos actualizar una fila arbitraria (o aleatoria ) , no tiene sentido esperar en absoluto.

Podemos desbloquear la situación con la ayuda de bloqueos de asesoramiento :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

De esta manera, la siguiente fila no bloqueada aún se actualizará. Cada transacción obtiene una nueva fila para trabajar. Recibí ayuda de la República Checa Postgres Wiki para este truco.

idsiendo cualquier bigintcolumna única (o cualquier tipo con una conversión implícita como int4o int2).

Si se usan bloqueos de aviso para varias tablas en su base de datos al mismo tiempo, desambigüe con pg_try_advisory_xact_lock(tableoid::int, id)- idsiendo único integeraquí.
Como tableoides una bigintcantidad, teóricamente puede desbordarse integer. Si eres lo suficientemente paranoico, úsalo (tableoid::bigint % 2147483648)::inten su lugar, dejando una "colisión hash" teórica para el verdadero paranoico ...

Además, Postgres es libre de probar las WHEREcondiciones en cualquier orden. Se podría probar pg_try_advisory_xact_lock()y adquirir un bloqueo antes status = 'standby' , lo que podría dar lugar a bloques de consulta adicionales en filas no relacionadas, en donde status = 'standby'no es cierto. Pregunta relacionada sobre SO:

Por lo general, puedes ignorar esto. Para garantizar que solo las filas calificadas estén bloqueadas, puede anidar el predicado (s) en un CTE como el anterior o una subconsulta con el OFFSET 0pirateo (evita la inserción) . Ejemplo:

O (más barato para escaneos secuenciales) anida las condiciones en una CASEdeclaración como:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Sin embargo, el CASEtruco también evitaría que Postgres use un índice status. Si dicho índice está disponible, no necesita anidamiento adicional para comenzar: solo las filas calificadas se bloquearán en un escaneo de índice.

Como no puede estar seguro de que se use un índice en cada llamada, puede simplemente:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

El CASEes lógicamente redundante, pero sirve al propósito discutido.

Si el comando es parte de una transacción larga, considere los bloqueos a nivel de sesión que pueden (y deben) liberarse manualmente. Por lo tanto, puede desbloquear tan pronto como haya terminado con la fila bloqueada: pg_try_advisory_lock()ypg_advisory_unlock() . El manual:

Una vez adquirido a nivel de sesión, se mantiene un bloqueo de aviso hasta que se libere explícitamente o la sesión finalice.

Relacionado:

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.