Geschiedenis van de vraag.
Adviesvergrendelingen verschenen voor het eerst in PostgreSQL 8.2 om lichte, applicatie-niveau synchronisatieprimitieven te bieden die buiten het MVCC tuple zichtbaarheidssysteem functioneren. Ze zijn ontworpen voor workflows zoals wachtrijverwerking en idempotente invoer, waarbij tabelgebaseerde vergrendeling semantisch ongepast of prestatiebelemmerend zou zijn. In tegenstelling tot rij-niveau vergrendelingen die zijn gekoppeld aan specifieke tabel tuples en worden geregistreerd in de xmax systeemkolom, bevinden adviesvergrendelingen zich volledig binnen de gedeelde-geheugenvergrendelmanager, waardoor ze een mechanisme bieden om toegang tot abstracte middelen te regelen zonder dode tuples of WAL verkeer te genereren.
Het probleem.
In hoog-concurrentie idempotente invoer pipelines zorgt het afdwingen van uniciteit op bedrijfs sleutels (bijv. externe UUIDs) via traditionele INSERT ... ON CONFLICT of SELECT FOR UPDATE voor ernstige knelpunten. Rij-niveau benaderingen vereisen schrijfoperaties naar de heap om vergrendelbits in te stellen, wat leidt tot bloat van tabellen, versnelt de VACUUM druk en veroorzaakt hotspots in unieke indexen tijdens conflictoplossing. De uitdaging is om wederzijdse uitsluiting te bieden voor logische entiteiten—zoals een gehashte bedrijfs sleutel—zonder de opslaglaag aan te raken, terwijl ervoor gezorgd wordt dat vergrendelingsfouten geen middelen lekken naar persistente verbinding pools.
De oplossing.
De kritische eigenschap is dat adviesvergrendelingen exclusief worden opgeslagen in de LOCKTAG hashtabel binnen gedeeld geheugen, met gebruik van LOCKMETHOD_ADVISORY, en daarom nooit de onderliggende relatiepagina's wijzigen. Door pg_advisory_xact_lock(hashtext(business_key)) te gebruiken, verwerft de applicatie een transactie-gescopeerde mutex die automatisch wordt vrijgegeven bij COMMIT of ROLLBACK, waardoor het lekken van vergrendelingen dat gepaard gaat met sessie-niveau pg_advisory_lock wordt voorkomen. Deze aanpak elimineert tabelbloat en indexinhouding, omdat de vergrendeling alleen bestaat als een lichte invoer in het geheugen, zoals hieronder geïllustreerd:
BEGIN; -- Verkrijg een transactiegebonden vergrendeling op de gehashte bedrijfs sleutel SELECT pg_advisory_xact_lock(hashtext('a1b2c3d4')); -- Veilig om in te voegen; geen unieke indexinhouding als een andere sessie de vergrendeling heeft INSERT INTO events (business_key, payload) VALUES ('a1b2c3d4', '{"event":"click"}') ON CONFLICT (business_key) DO NOTHING; COMMIT;
Het dataplatformteam bij een telemetriebedrijf moest exact-een verwerking garanderen voor 50.000 evenementen per seconde die uit Kafka in PostgreSQL werden ingevoerd, waarbij elk evenement een door de klant gegenereerde UUID droeg die als de idempotentie sleutel diende. Aanvankelijke loadtests met gebruik van INSERT ... ON CONFLICT DO NOTHING op een unieke UUID kolom veroorzaakten ernstige tail latency door spinlock-inhouding op de unieke B-tree index en snel oplopende bloat door HOT update mislukkingen. De WAL generatie snelheid verdubbelde tijdens piekuren, wat de replicatievertraging en opslagcapaciteit bedreigde.
Een voorgestelde oplossing hield in dat er vooraf werd gecontroleerd op de aanwezigheid van de sleutel met SELECT * FROM events WHERE business_key = $1 FOR UPDATE, waarna alleen werd ingevoerd als het resultaat leeg was. Hoewel dit duplicaten verhinderde, dwong het elke schrijver om een rijvergrendeling te verkrijgen op ofwel de bestaande rij of een surrogaat reserveringsrij, waardoor een enorme hotspot op de pagina's van de reserveringstabel werd gecreëerd. De aanpak genereerde aanzienlijke tabelbloat—waarbij VACUUM dode tuples elke vijftien minuten moest terugwinnen—en kon race-voorwaarden tussen de controle en de invoer niet voorkomen zonder de vergrendeling voor de gehele transactieduur vast te houden, wat de throughput ernstig beperkte.
Het architectuurteam stelde voor de coördinatie naar een externe Redis cache te verplaatsen met gebruik van SETNX operaties om invoer te controleren. Dit elimineerde databasebloat en verminderde de belasting van PostgreSQL, maar introduceerde kritieke foutmodi: netwerkpartities tussen het Redis cluster en de database konden dubbele invoeren toestaan wanneer de Redis vergrendeling verleende, maar de PostgreSQL transactie nog niet was gecommitteerd. Bovendien voegde het handhaven van consistentie tussen twee gedistribueerde systemen operationele complexiteit toe en vereiste het implementeren van Redlock of vergelijkbare algoritmen, wat de latentie met ongeveer 5 milliseconden per operatie verhoogde.
Het gekozen ontwerp maakte gebruik van de native adviesvergrendelingen van PostgreSQL via pg_advisory_xact_lock(hashtext(business_key)), waarbij een transactiegebonden vergrendeling op de gehashte UUID werd verkregen voordat geprobeerd werd in te voegen. Omdat deze vergrendelingen alleen in gedeeld geheugen leven en de heap niet aanraken, brengen ze geen opslagoverhead met zich mee en worden ze automatisch vrijgegeven bij beëindiging van de transactie, waardoor het lekken van vergrendelingen dat werd waargenomen met sessie-niveau vergrendelingen wordt voorkomen. Om ondetecteerbare dödlokken te vermijden, sorteerde de applicatielaag alle UUIDs in elke batch op hun gehashte gehele waarde voordat vergrendelingen werden verkregen, om een wereldwijd ordeningsprotocol tussen alle gelijktijdige werkers te waarborgen.
Adviesvergrendelingen werden geselecteerd omdat ze de laagste latentie (sub-milliseconde acquisitie) en nul opslagbijwerkingen boden, terwijl ze strikte correctheid handhaafden zonder externe afhankelijkheden. In tegenstelling tot de Redis benadering was de levensduur van de vergrendeling gebonden aan de database transactie, wat atomiteit tussen vergrendeling acquisitie en invoer commit garandeert. In tegenstelling tot SELECT FOR UPDATE werd er geen tabelbloat gegenereerd, en in tegenstelling tot rauwe ON CONFLICT werd de unieke index nooit onder druk gezet door conflicterende gelijktijdige invoeringen omdat serialisatie vóór de heaptoegang plaatsvond.
Na implementatie kon de invoer pipeline 80.000 evenementen per seconde aan met p99 latentie onder de 10 milliseconden, vergeleken met eerdere pieken van 200 ms tijdens inhoudingspieken. Tabelbloat daalde tot verwaarloosbare niveaus, waardoor autovacuum alleen tijdens daluren hoefde te draaien, en het WAL volume daalde met 40%, wat de archiveringskosten en replica-vertraging aanzienlijk verminderde. Het systeem handhaafde exact-een semantiek door meerdere database herstarts en verbinding pool fluctuaties zonder een enkele duplicaat evenement of dödlok-geïnduceerde timeout.
Waarom het gebruik van pg_advisory_lock (sessie-gescopeerd) in plaats van pg_advisory_xact_lock het risico op uitputting van de verbindingspool en dubbele invoering in een hoge-doorvoer werkerarchitectuur met zich meebrengt?
Kandidaten vergeten vaak dat pg_advisory_lock aanhoudt totdat deze expliciet wordt ontgrendeld of de sessie wordt losgekoppeld, zelfs als de transactie afgebroken wordt. In een gepoolde omgeving waar werkers langlevende verbindingen hergebruiken, laat een logische fout of uitzondering die de ontgrendeloproep omzeilt de vergrendeling onbeperkt vastgehouden, waardoor volgende werkers die dezelfde bedrijfs sleutel verwerken voor altijd moeten wachten. pg_advisory_xact_lock moet in plaats daarvan worden gebruikt omdat het de levensduur van de vergrendeling aan de transactierand verbindt, wat automatische vrijgave bij ROLLBACK garandeert en het lekken van mutex voorkomt dat anders de werkerpool zou uithongeren en de invoer pipeline zou stilleggen.
Hoe leidt de afwezigheid van een garantie voor totale ordening bij het verwerven van meerdere adviesvergrendelingen tot ondetecteerbare dödlokken, en welk specifiek applicatiepatroon elimineert dit gevaar?
In tegenstelling tot rij-niveau dödlokken die door de deadlock_timeout detector van PostgreSQL worden opgelost door een slachtoffertransactie te doden, zijn adviesvergrendel-dödlokken onzichtbaar voor de motor omdat ze optreden in door de gebruiker gedefinieerde namespaces. Als Werker A hulpbron X dan Y vergrendelt, terwijl Werker B Y dan X vergrendelt, wachten beide sessies oneindig zonder foutmelding. Het verplichte patroon is om alle hulpbronidentificaties (bijv. hashtext(uuid) waarden) in een strikte monotone volgorde (oplopend of aflopend) over de gehele applicatie te sorteren voordat er vergrendelverzoeken worden ingediend. Deze wereldwijde ordening zorgt ervoor dat wacht-voor-grafieken acyclisch blijven, waardoor cirkelvormige afhankelijkheden onmogelijk worden gemaakt en het risico op stille vastlopers wordt geëlimineerd.
Welke beperking van gedeeld geheugen beperkt het aantal adviesvergrendelingen dat een enkele transactie kan vasthouden, en hoe manifesteert het overschrijden van max_locks_per_transaction zich in vergelijking met uitputting van rij-niveau vergrendelingen?
Veel kandidaten gaan ervan uit dat adviesvergrendelingen oneindig zijn, maar ze verbruiken invoeren in de gedeelde vergrendelings tabel die wordt beheerd door de configuratieparameter max_locks_per_transaction (standaard 64). Het vasthouden van meer vergrendelingen dan deze limiet in één transactie resulteert in ERROR: out of shared memory (SQLSTATE 53200), wat de transactie onmiddellijk afbreekt. Dit staat in contrast met rij-niveau vergrendelingen, waarbij overschrijding van limieten doorgaans een vergrendelingsupgrade of wachten activeert afhankelijk van lock_timeout, maar niet een vaste gedeelde geheugenpool uitput. De mitigatie houdt in dat operaties in kleinere subtransacties worden gebundeld of dat meerdere logische middelen onder één adviesvergrendelingssleutel worden geaggregeerd via samengestelde hashing, in plaats van te proberen duizenden individuele sleutels tegelijkertijd te vergrendelen.