2017-08-10 2 views
3

Parmi les nombreuses choses que nous faisons avec Postgres au travail, nous l'utilisons comme cache pour certains types de requêtes distantes. Notre schéma est:Comment deux requêtes DELETE peuvent-elles être bloquées dans Postgres?

CREATE TABLE IF NOT EXISTS cache (
    key VARCHAR(256) PRIMARY KEY, 
    value TEXT NOT NULL, 
    ttl TIMESTAMP DEFAULT NULL 
); 

CREATE INDEX IF NOT EXISTS idx_cache_ttl ON cache(ttl); 

Cette table ne comporte pas de déclencheurs ou de clés étrangères. Les mises à jour sont généralement:

INSERT INTO cache (key, value, ttl) 
VALUES ('Ethan is testing8393645', '"hi6286166"', sec2ttl(300)) 
ON CONFLICT (key) DO UPDATE 
SET value = '"hi6286166"', ttl = sec2ttl(300); 

(Où sec2ttl est défini comme :)

CREATE OR REPLACE FUNCTION sec2ttl(seconds FLOAT) 
RETURNS TIMESTAMP AS $$ 
BEGIN 
    IF seconds IS NULL THEN 
     RETURN NULL; 
    END IF; 
    RETURN now() + (seconds || ' SECOND')::INTERVAL; 
END; 
$$ LANGUAGE plpgsql; 

Interrogation le cache se fait dans une transaction comme celle-ci:

BEGIN; 
DELETE FROM cache WHERE ttl IS NOT NULL AND now() > ttl; 
SELECT value FROM cache WHERE key = 'Ethan is testing6460437'; 
COMMIT; 

Il y a quelques choses à ne pas Aime à propos de cette conception - le DELETE passe dans le cache "lit", l'index sur cache.ttl n'est pas ascendant ce qui le rend un peu usele ss, (edit: ASC est la valeur par défaut, merci wargre!) plus le fait que nous utilisons Postgres comme cache du tout. Mais tout cela aurait été acceptable, sauf que nous avons commencé à recevoir la production, les blocages qui ont tendance à ressembler à ceci:

ERROR: deadlock detected 
DETAIL: Process 12750 waits for ShareLock on transaction 632693475; blocked by process 10080. 
Process 10080 waits for ShareLock on transaction 632693479; blocked by process 12750. 
HINT: See server log for query details. 
CONTEXT: while deleting tuple (426,1) in relation "cache" 
[SQL: 'DELETE FROM cache WHERE ttl IS NOT NULL AND now() > ttl;'] 

analysant les journaux plus indique bien que les deux transactions effectuaient cette opération DELETE.

Pour autant que je peux dire:

  • Mes transactions sont en READ COMMITTED mode d'isolation.
  • ShareLocks sont saisis par une transaction pour indiquer qu'elle veut muter des lignes qu'une autre transaction a mutées (c'est-à-dire verrouillées).
  • En fonction de la sortie d'une requête EXPLAIN, les ShareLocks doivent être saisis par les deux transactions DELETE dans l'ordre physique.
  • Le blocage indique que les deux requêtes verrouillent les lignes dans un ordre différent.

Si tout est correct, alors une transaction simultanée a changé l'ordre physique des lignes. Je vois qu'un UPDATE peut déplacer une rangée vers une position physique plus tôt ou plus tard, mais dans mon application, le UPDATE s enlève toujours des rangées par les DELETE s (parce qu'elles prolongent toujours la durée de vie d'une ligne). Si les lignes étaient auparavant dans l'ordre physique et que vous en supprimez une, il vous reste l'ordre physique. De même pour DELETE. Nous ne faisons aucune VACUUM ou toute autre opération que vous pourriez vous attendre à réorganiser les lignes.

Basé sur Avoiding PostgreSQL deadlocks when performing bulk update and delete operations, j'ai essayé de changer les DELETE questions à:

DELETE FROM cache c 
USING (
    SELECT key 
    FROM cache 
    WHERE ttl IS NOT NULL AND now() > ttl 
    ORDER BY ttl ASC 
    FOR UPDATE 
) del 
WHERE del.key = c.key; 

Cependant, je suis toujours en mesure d'obtenir localement des blocages. Donc, généralement, comment deux requêtes DELETE peuvent-elles être bloquées? Est-ce parce qu'ils se verrouillent dans un ordre indéfini, et si oui, comment appliquer un ordre spécifique?

+0

Qu'en est-il de la gestion de votre cache dans une connexion avec autocommit et sans transaction? (btw, l'index est correct, il est utilisé par votre dlete) – wargre

+0

Je pense que même si je n'ai pas de transaction, Postgres crée une transaction autour de chaque requête, n'est-ce pas? Et les DELETE semblent se bloquer par eux-mêmes .. (BTW, merci, je vois ASC est la valeur par défaut pour les indices.) – Ethan

+1

Le point de la 'SELECT ... FOR UPDATE' est d'appliquer un ordre globalement cohérent pour toutes les acquisitions de serrures . Si deux valeurs 'ttl' coïncident, l'ordre des lignes n'est pas défini et si une valeur' ttl' est mise à jour, alors l'ordre, bien défini, peut différer entre les transactions concurrentes. Essayez 'ORDER BY key' à la place. –

Répondre

0

Vous devriez plutôt ignorer les entrées du cache expiré, de sorte que vous ne serez pas dépendre d'une opération de suppression fréquente pour l'expiration du cache:

SELECT value 
FROM cache 
WHERE 
    key = 'Ethan is testing6460437' 
    and (ttl is null or ttl<now()); 

Et un autre emploi que les clés choisit périodiquement pour supprimer sauter les clés déjà verrouillés, ce qui a soit forcer un ordre bien défini de supprimer une ligne ou, mieux, sautez déjà verrouillé pour les lignes de mise à jour:

with delete_keys as (
    select key from cache 
    where 
    ttl is not null 
    and now()>ttl 
    for update skip locked 
) 
delete from cache 
where key in (select key from delete_keys); 

Si vous ne pouvez pas planifier ce périodique vous devez exécuter ce nettoyage comme une fois tous les 1000 courses au hasard de votre choisir que ry, comme ceci:

create or replace function delete_expired_cache() 
returns void 
language sql 
as $$ 
    with delete_keys as (
    select key from cache 
    where 
     ttl is not null 
     and now()>ttl 
    for update skip locked 
) 
    delete from cache 
    where key in (select key from delete_keys); 
$$; 

SELECT value 
FROM cache 
WHERE 
    key = 'Ethan is testing6460437' 
    and (ttl is null or ttl<now()); 
select delete_expired_cache() where random()<0.001; 

Vous devriez éviter les écritures, car elles sont chères. Ne supprimez pas le cache si souvent.


Aussi, vous devez utiliser timestamp with time zone type (ou timestamptz pour faire court) au lieu de simples timestamp - surtout si vous ne savez pas pourquoi - un timestamp n'est pas la chose la plus pense qu'il est - le blâme standard SQL.

+0

Merci - mais je connais déjà les problèmes avec le design. Ma question n'est pas comment réparer la conception, mais pourquoi cette conception provoque-t-elle une impasse? (Aussi, merci pour l'astuce concernant timestamptz, mais nous 'SET TIMEZONE TO UTC' sur notre base de données, notre application n'expose pas vraiment d'horodatage sauf ceux d'UTC.) – Ethan