Pukul dua pagi, cron server utama menjalankan pekerjaan maintenance database — tepat ketika server cadangan yang baru aktif kembali menjalankan pekerjaan yang sama. Dua proses menghitung bunga, dua proses membangun ulang cache, dan hasilnya berantakan. Masalah ini bukan soal perlindungan data — kunci baris dan kunci tabel sudah menungani itu — melainkan soal kesepakatan antar proses aplikasi: “hanya satu yang boleh jalan”. PostgreSQL menjawabnya dengan advisory lock: kunci yang tidak menempel pada baris maupun tabel mana pun, hanya berupa angka kunci yang maknanya ditentukan aplikasi itu sendiri. Database hanya berjanji menyimpan dan mengecek kuncinya — aplikasi yang harus menghormati kesepakatan. Chapter ini membedah dua tingkat kunci advisory (sesi dan transaksi), bentuk kunci tunggal maupun ganda, dan pola pemilih-pemimpin yang menjadikannya alat koordinasi lintas proses.
Menyiapkan Tabel Latihan #
Demo memakai tabel rekening yang sudah akrab. Konteks pekerjaannya: sebuah pekerjaan maintenance yang menyetorkan bunga Rp5.000 ke semua rekening tabungan — pekerjaan yang boleh dijalankan hanya satu proses pada satu waktu:
CREATE TABLE rekening (
id integer PRIMARY KEY,
nama text NOT NULL,
jenis text NOT NULL,
saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
CONSTRAINT saldo_ewallet_maks
CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);
INSERT INTO rekening (id, nama, jenis, saldo) VALUES
(1, 'Budi Santoso', 'tabungan', 500000),
(2, 'Siti Aminah', 'tabungan', 300000),
(3, 'Dewi Lestari', 'e-wallet', 950000);
SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
id | nama | jenis | saldo
----+--------------+----------+-----------
1 | Budi Santoso | tabungan | 500000.00
2 | Siti Aminah | tabungan | 300000.00
3 | Dewi Lestari | e-wallet | 950000.00
(3 rows)Kunci Advisory Level Sesi #
Fungsi pg_advisory_lock(kunci) mengambil kunci advisory dan terus memegangnya — melampaui batas transaksi — sampai kunci itu dilepas lewat pg_advisory_unlock(kunci) atau sesi terputus. Kuncinya berupa satu angka bigint; angka berapa pun boleh, maknanya murni konvensi aplikasi. Catalog pg_locks mencatat kunci ini dengan locktype = 'advisory':
-- Kunci advisory level sesi: dipegang sampai dilepas atau sesi terputus.
SELECT pg_advisory_lock(987654321);
SELECT locktype, classid, objid, objsubid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY classid, objid;
SELECT pg_advisory_unlock(987654321);
SELECT count(*) AS sisa_kunci_advisory
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database());Output yang Dihasilkan:
pg_advisory_lock
------------------
(1 row)
locktype | classid | objid | objsubid | mode | granted
----------+---------+-----------+----------+---------------+---------
advisory | 0 | 987654321 | 1 | ExclusiveLock | t
(1 row)
pg_advisory_unlock
--------------------
t
(1 row)
sisa_kunci_advisory
---------------------
0
(1 row)Tiga hasil berturut-turut menceritakan siklus hidupnya. pg_advisory_lock(987654321) mengambil kunci — fungsinya mengembalikan void, jadi satu-satunya jejaknya adalah baris di pg_locks. Baris itu menunjukkan kunci advisory dengan classid = 0 dan objid = 987654321 (angka kuncinya), mode ExclusiveLock, dan granted = t. Lalu pg_advisory_unlock(987654321) mengembalikan t — sukses melepas — dan penghitungan terakhir menegaskan pg_locks sudah bersih: kunci level sesi memang tidak perlu transaksi untuk hidup, dan tidak hilang sendiri sebelum dilepas.
pg_try_advisory_lock dan Perilaku Re-Entrant #
pg_advisory_lock bersabar: bila kunci sedang dipegang proses lain, fungsi ini menunggu sampai bebas. Variannya, pg_try_advisory_lock, tidak pernah menunggu — langsung melaporkan true atau false. Fungsi ini pilar utama pola pekerja tunggal: kandidat pekerja mencoba kunci, yang gagal langsung menyerah dan melanjutkan hidup. Satu perilaku penting yang jarang disadari: sesi yang sama boleh mengambil ulang kunci yang sedang dipegangnya sendiri, dan pelepasannya pun harus dilakukan sebanyak pengambilannya:
-- pg_try_advisory_lock: tidak pernah menunggu, langsung melaporkan hasil.
-- Kunci yang sama bisa diambil ulang sesi yang sama (re-entrant):
-- jumlah entri di pg_locks tetap satu, tetapi pembebasannya harus diulang dua kali.
SELECT pg_try_advisory_lock(987654321) AS kunci_pertama;
SELECT pg_try_advisory_lock(987654321) AS kunci_kedua;
SELECT count(*) AS jumlah_entri
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 987654321;
SELECT pg_advisory_unlock(987654321) AS lepas_pertama;
SELECT pg_advisory_unlock(987654321) AS lepas_kedua;
SELECT pg_advisory_unlock(987654321) AS lepas_ketiga;Output yang Dihasilkan:
kunci_pertama
---------------
t
(1 row)
kunci_kedua
-------------
t
(1 row)
jumlah_entri
--------------
1
(1 row)
lepas_pertama
---------------
t
(1 row)
lepas_kedua
-------------
t
(1 row)
lepas_ketiga
--------------
f
(1 row)Percobaan pertama dan kedua sama-sama t — PostgreSQL menghitung berapa kali sesi yang sama mengambil kunci, bukan menolaknya. Entri di pg_locks tetap satu baris. Konsekuensinya terlihat di tiga pembebasan: lepas_pertama dan lepas_kedua sama-sama t (dua pengambilan = dua pelepasan), sedangkan lepas_ketiga bernilai f — kunci sudah tidak dipegang lagi. Saat pelepasan ketiga itu dijalankan lewat psql, server juga menampilkan WARNING: you don't own a lock of type ExclusiveLock di keluaran kesalahan (stderr) — peringatan itu tidak ikut blok output karena psql menuliskannya terpisah dari hasil query.
Level Transaksi: Lepas Otomatis saat Commit #
Kebocoran kunci level sesi adalah bug yang menjengkelkan: proses lupa memanggil unlock, sesi tetap hidup di connection pool, dan seluruh pekerjaan berikutnya menganggap “maintenance sedang berjalan” selamanya. Untuk pekerjaan yang seluruh hidupnya berada dalam satu transaksi, PostgreSQL menyediakan pg_advisory_xact_lock — kuncinya dilepas otomatis tepat saat transaksi berakhir, baik lewat COMMIT maupun ROLLBACK. Demo berikut menjalankan pekerjaan bunga lengkap dengan penjaganya:
-- Kunci advisory level transaksi: dilepas otomatis saat COMMIT/ROLLBACK.
-- Pola pekerjaan maintenance: ambil kunci, kerjakan update, tutup transaksi -
-- kunci ikut lenyap tanpa perintah unlock.
BEGIN;
SELECT pg_advisory_xact_lock(1234567);
SELECT count(*) AS kunci_selama_transaksi
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 1234567;
-- pekerjaan yang dijaga kunci: setoran bunga Rp5.000 untuk rekening tabungan
UPDATE rekening SET saldo = saldo + 5000 WHERE jenis = 'tabungan';
COMMIT;
SELECT count(*) AS kunci_setelah_commit
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 1234567;
SELECT id, nama, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
BEGIN
pg_advisory_xact_lock
-----------------------
(1 row)
kunci_selama_transaksi
------------------------
1
(1 row)
UPDATE 2
COMMIT
kunci_setelah_commit
----------------------
0
(1 row)
id | nama | saldo
----+--------------+-----------
1 | Budi Santoso | 505000.00
2 | Siti Aminah | 305000.00
3 | Dewi Lestari | 950000.00
(3 rows)Selama transaksi berjalan, pg_locks mencatat satu entri kunci dengan objid = 1234567. Pekerjaan bunganya sendiri menuntaskan UPDATE 2 — Budi dan Siti masing-masing bertambah Rp5.000. Tidak ada perintah pg_advisory_unlock sama sekali, tetapi penghitungan setelah COMMIT menunjukkan 0: kunci level transaksi ikut lenyap saat transaksi ditutup. Perbandingan ringkas kedua tingkat:
| Aspek | pg_advisory_lock (sesi) | pg_advisory_xact_lock (transaksi) |
|---|---|---|
| Masa berlaku | Sampai pg_advisory_unlock atau sesi terputus | Sampai COMMIT atau ROLLBACK |
| Bisa dilupakan dan bocor | Bisa — sumber bug klasik di connection pool | Tidak — dilepas otomatis |
| Pengambilan ulang oleh sesi yang sama | Diizinkan, dihitung berlapis | Diizinkan |
| Cocok untuk | Pekerjaan lintas transaksi, sesi panjang | Pekerjaan dalam satu transaksi |
Kunci Ganda: Kelompok Pekerjaan dengan Dua Angka #
Satu angka bigint tidak selalu cukup untuk memberi makna pada kunci. Bentuk kedua, pg_advisory_lock(int, int), menerima dua angka 32-bit — pola yang nyaman untuk kelompok pekerjaan: angka pertama menandai jenis pekerjaan, angka kedua nomor uniknya. Maintenance bisa memakai kelompok 11, rebuild cache kelompok 12, dan keduanya tidak akan pernah saling berebut:
-- Bentuk kunci ganda (int, int): dua angka 32-bit sebagai kunci.
-- classid menampung angka pertama, objid angka kedua -
-- berguna untuk pemisahan kelompok kunci per jenis pekerjaan.
SELECT pg_advisory_lock(11, 100);
SELECT locktype, classid, objid, objsubid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY classid, objid;
SELECT pg_advisory_unlock(11, 100);Output yang Dihasilkan:
pg_advisory_lock
------------------
(1 row)
locktype | classid | objid | objsubid | mode | granted
----------+---------+-------+----------+---------------+---------
advisory | 11 | 100 | 2 | ExclusiveLock | t
(1 row)
pg_advisory_unlock
--------------------
t
(1 row)Di pg_locks, dua angka itu tersimpan di kolom classid (angka pertama, 11) dan objid (angka kedua, 100), dengan objsubid = 2 yang menandai bentuk kunci ganda — bandingkan dengan objsubid = 1 pada bentuk satu angka di demo pertama. Ruang angkanya pun luas: dua angka 32-bit memberi ruang kelompok dan nomor yang jauh lebih terstruktur daripada satu angka besar yang harus dihafal maknanya. Kedua bentuk tidak saling bertukar: kunci 987654321 dan kunci (11, 100) adalah dua kunci berbeda, berapa pun nilainya.
Dua Pekerja, Satu Pekerjaan #
Sisa satu pertanyaan: apa yang sebenarnya dirasakan pekerja kedua saat kunci sudah dipegang orang lain? Karena pg_try_advisory_lock tidak menunggu, jawabannya datang seketika. Demo berikut mensimulasikan dua pekerja: pekerja A mengambil kunci dan tetap tersambung; pekerja B mencoba kunci yang sama:
#!/bin/bash
# Simulasi dua pekerja maintenance yang sama-sama ingin mengunci job:
# pekerja A sudah memegang kunci advisory dan tetap tersambung;
# pekerja B memanggil pg_try_advisory_lock dan mendapat jawaban false.
: "${PGDATABASE:=latihan}"
export PGDATABASE
TMPD=$(mktemp -d)
mkfifo "$TMPD/komando.fifo"
# Pekerja A: sesi psql yang tetap hidup, perintah dikirim lewat fifo
PGAPPNAME=pekerja-a psql -X < "$TMPD/komando.fifo" > "$TMPD/a.log" 2>&1 &
SESIA=$!
exec 3>"$TMPD/komando.fifo"
printf 'SELECT pg_advisory_lock(987654321);\n' >&3
# Tunggu sampai kunci advisory benar-benar terpasang di pg_locks
for i in $(seq 1 100); do
n=$(psql -X -tA -c "SELECT count(*) FROM pg_locks
WHERE locktype = 'advisory' AND objid = 987654321
AND database = (SELECT oid FROM pg_database
WHERE datname = current_database())")
[ "$n" = "1" ] && break
sleep 0.05
done
# Pekerja B: mencoba kunci yang sama tanpa menunggu
PGAPPNAME=pekerja-b psql -X \
-c "SELECT pg_try_advisory_lock(987654321) AS kunci_berhasil;" \
> "$TMPD/b.log" 2>&1
# Pekerja A melepas kunci setelah pekerjaannya selesai
printf 'SELECT pg_advisory_unlock(987654321);\n' >&3
exec 3>&-
wait $SESIA
echo "== Pekerja A (memegang kunci, lalu melepasnya) =="
cat "$TMPD/a.log"
echo
echo "== Pekerja B (kunci sudah dipegang orang lain) =="
cat "$TMPD/b.log"
rm -rf "$TMPD"Output yang Dihasilkan:
== Pekerja A (memegang kunci, lalu melepasnya) ==
pg_advisory_lock
------------------
(1 row)
pg_advisory_unlock
--------------------
t
(1 row)
== Pekerja B (kunci sudah dipegang orang lain) ==
kunci_berhasil
----------------
f
(1 row)Pekerja B menerima kunci_berhasil = f dan bebas memutuskan: mundur dengan tenang, mencoba lagi setelah jeda, atau sekadar mencatat bahwa pekerjaan sedang dikerjakan orang lain. Inilah bahan dasar pola leader election: sekelompok proses yang sama-sama bangun tiap jadwal, masing-masing mencoba satu kunci, dan hanya pemegang kunci yang bekerja — sisanya kembali tidur tanpa menghabiskan apa pun. Skrip demo menunggu kunci A muncul di pg_locks sebelum menghidupkan pekerja B, itulah cara skrip memastikan urutan kejadian yang konsisten.
Advisory lock melengkapi tiga alat kendali yang sudah Anda kuasai — kunci baris untuk baris yang diperebutkan, kunci tabel untuk struktur yang diubah, dan kunci advisory untuk kesepakatan antar proses. Satu tantangan tersisa di urusan konkurensi: bagaimana bila aplikasi menuntut hasil yang setara dengan eksekusi berurutan tanpa harus mengunci apa pun sejak awal? Isolation level Serializable menjawabnya dengan mendeteksi tabrakan logika dan meminta transaksi korban diulang — topik chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkasCREATE TABLE rekening (
id integer PRIMARY KEY,
nama text NOT NULL,
jenis text NOT NULL,
saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
CONSTRAINT saldo_ewallet_maks
CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);
INSERT INTO rekening (id, nama, jenis, saldo) VALUES
(1, 'Budi Santoso', 'tabungan', 500000),
(2, 'Siti Aminah', 'tabungan', 300000),
(3, 'Dewi Lestari', 'e-wallet', 950000);
SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;
kunci-dua-kunci.sql
Unduh berkas-- Bentuk kunci ganda (int, int): dua angka 32-bit sebagai kunci.
-- classid menampung angka pertama, objid angka kedua -
-- berguna untuk pemisahan kelompok kunci per jenis pekerjaan.
SELECT pg_advisory_lock(11, 100);
SELECT locktype, classid, objid, objsubid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY classid, objid;
SELECT pg_advisory_unlock(11, 100);
kunci-sesi.sql
Unduh berkas-- Kunci advisory level sesi: dipegang sampai dilepas atau sesi terputus.
SELECT pg_advisory_lock(987654321);
SELECT locktype, classid, objid, objsubid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database())
ORDER BY classid, objid;
SELECT pg_advisory_unlock(987654321);
SELECT count(*) AS sisa_kunci_advisory
FROM pg_locks
WHERE locktype = 'advisory'
AND database = (SELECT oid FROM pg_database WHERE datname = current_database());
kunci-xact.sql
Unduh berkas-- Kunci advisory level transaksi: dilepas otomatis saat COMMIT/ROLLBACK.
-- Pola pekerjaan maintenance: ambil kunci, kerjakan update, tutup transaksi -
-- kunci ikut lenyap tanpa perintah unlock.
BEGIN;
SELECT pg_advisory_xact_lock(1234567);
SELECT count(*) AS kunci_selama_transaksi
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 1234567;
-- pekerjaan yang dijaga kunci: setoran bunga Rp5.000 untuk rekening tabungan
UPDATE rekening SET saldo = saldo + 5000 WHERE jenis = 'tabungan';
COMMIT;
SELECT count(*) AS kunci_setelah_commit
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 1234567;
SELECT id, nama, saldo FROM rekening ORDER BY id;
pekerjaan-tunggal.sh
Unduh berkas#!/bin/bash
# Simulasi dua pekerja maintenance yang sama-sama ingin mengunci job:
# pekerja A sudah memegang kunci advisory dan tetap tersambung;
# pekerja B memanggil pg_try_advisory_lock dan mendapat jawaban false.
: "${PGDATABASE:=latihan}"
export PGDATABASE
TMPD=$(mktemp -d)
mkfifo "$TMPD/komando.fifo"
# Pekerja A: sesi psql yang tetap hidup, perintah dikirim lewat fifo
PGAPPNAME=pekerja-a psql -X < "$TMPD/komando.fifo" > "$TMPD/a.log" 2>&1 &
SESIA=$!
exec 3>"$TMPD/komando.fifo"
printf 'SELECT pg_advisory_lock(987654321);\n' >&3
# Tunggu sampai kunci advisory benar-benar terpasang di pg_locks
for i in $(seq 1 100); do
n=$(psql -X -tA -c "SELECT count(*) FROM pg_locks
WHERE locktype = 'advisory' AND objid = 987654321
AND database = (SELECT oid FROM pg_database
WHERE datname = current_database())")
[ "$n" = "1" ] && break
sleep 0.05
done
# Pekerja B: mencoba kunci yang sama tanpa menunggu
PGAPPNAME=pekerja-b psql -X \
-c "SELECT pg_try_advisory_lock(987654321) AS kunci_berhasil;" \
> "$TMPD/b.log" 2>&1
# Pekerja A melepas kunci setelah pekerjaannya selesai
printf 'SELECT pg_advisory_unlock(987654321);\n' >&3
exec 3>&-
wait $SESIA
echo "== Pekerja A (memegang kunci, lalu melepasnya) =="
cat "$TMPD/a.log"
echo
echo "== Pekerja B (kunci sudah dipegang orang lain) =="
cat "$TMPD/b.log"
rm -rf "$TMPD"
try-lock.sql
Unduh berkas-- pg_try_advisory_lock: tidak pernah menunggu, langsung melaporkan hasil.
-- Kunci yang sama bisa diambil ulang sesi yang sama (re-entrant):
-- jumlah entri di pg_locks tetap satu, tetapi pembebasannya harus diulang dua kali.
SELECT pg_try_advisory_lock(987654321) AS kunci_pertama;
SELECT pg_try_advisory_lock(987654321) AS kunci_kedua;
SELECT count(*) AS jumlah_entri
FROM pg_locks
WHERE locktype = 'advisory' AND objid = 987654321;
SELECT pg_advisory_unlock(987654321) AS lepas_pertama;
SELECT pg_advisory_unlock(987654321) AS lepas_kedua;
SELECT pg_advisory_unlock(987654321) AS lepas_ketiga;