Postgres 17 - Memahami Transactions dan Locking
Memahami Transactions dan Locking
Pendahuluan
Transaksi dan penguncian (locking) adalah dua konsep utama dalam pengelolaan basis data, termasuk PostgreSQL. Keduanya memastikan data tetap konsisten, aman, dan dapat diakses secara konkuren oleh banyak pengguna tanpa konflik. Artikel ini dirancang untuk menjelaskan transaksi dan penguncian (locking) secara terstruktur, jelas, dan mudah dipahami.
Daftar Isi:
Apa Itu Transaksi di PostgreSQL?
Fitur-Fitur Transaksi
Perintah Dasar: BEGIN, COMMIT, ROLLBACK
Transaction Chaining
Savepoint
DDL Transaksional
Apa Itu Penguncian (Locking) dan Mengapa Penting?
Mekanisme Penguncian di PostgreSQL
Multi-Version Concurrency Control (MVCC)
Penguncian Baris (Row-Level Locking)
Penguncian Tabel (Table-Level Locking)
SELECT FOR UPDATE dan FOR SHARE
SKIP LOCKED
Advisory Locks
Tingkat Isolasi Transaksi
Read Committed
Repeatable Read
Serializable
Menangani Masalah: Deadlock dan Konflik Serialisasi
Optimasi Penyimpanan dan Pembersihan
VACUUM dan Autovacuum
Transaction Wraparound
VACUUM FULL
Kesimpulan
1. Apa Itu Transaksi di PostgreSQL?
Transaksi adalah sekumpulan perintah SQL yang dijalankan sebagai satu unit kerja. Semua perintah di dalamnya harus berhasil (commit) atau dibatalkan (rollback) jika ada yang gagal. Ini memastikan data tetap konsisten.
Di PostgreSQL, setiap perintah SQL adalah transaksi, bahkan perintah sederhana seperti SELECT
. Contoh:
SELECT now();
Perintah ini dijalankan sebagai transaksi tunggal. Fungsi now()
mengembalikan waktu transaksi, bukan waktu saat ini. Jika Anda ingin waktu nyata, gunakan clock_timestamp()
.
Untuk mengelompokkan beberapa perintah dalam satu transaksi, gunakan BEGIN
, COMMIT
, atau ROLLBACK
. Contoh:
BEGIN;
INSERT INTO users (name) VALUES ('Alice');
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
COMMIT;
Jika salah satu perintah gagal, Anda bisa menggunakan ROLLBACK
untuk membatalkan semua perubahan.
2. Fitur-Fitur Transaksi
a. Perintah Dasar: BEGIN, COMMIT, ROLLBACK
BEGIN: Memulai transaksi, memungkinkan beberapa perintah dijalankan sebagai satu unit.
COMMIT: Menyimpan semua perubahan dalam transaksi ke basis data.
ROLLBACK: Membatalkan semua perubahan dalam transaksi, mengembalikan data ke keadaan sebelumnya.
Contoh:
BEGIN;
INSERT INTO t_test (id) VALUES (1);
SELECT * FROM t_test; -- Melihat id = 1
ROLLBACK;
SELECT * FROM t_test; -- Tidak ada data, karena ROLLBACK
Alternatif untuk COMMIT
adalah END
, dan untuk ROLLBACK
adalah ABORT
. Keduanya memiliki fungsi yang sama.
b. Transaction Chaining
Fitur transaction chaining memungkinkan transaksi baru dimulai otomatis dengan properti yang sama setelah transaksi sebelumnya di-commit. Ini berguna untuk mengurangi jumlah perintah yang dikirim ke server, terutama dalam sistem dengan latensi tinggi.
Contoh:
BEGIN TRANSACTION READ ONLY;
SELECT 1;
COMMIT AND CHAIN; -- Memulai transaksi baru dengan mode READ ONLY
SELECT 1;
COMMIT AND NO CHAIN; -- Mengakhiri rantai
Perintah COMMIT AND CHAIN
mempertahankan pengaturan seperti READ ONLY
, sehingga Anda tidak perlu mengatur ulang.
c. Savepoint
Savepoint memungkinkan Anda membuat "titik simpan" dalam transaksi, sehingga Anda bisa kembali ke titik tersebut jika terjadi kesalahan tanpa membatalkan seluruh transaksi.
Contoh:
BEGIN;
INSERT INTO t_test (id) VALUES (1);
SAVEPOINT my_savepoint;
INSERT INTO t_test (id) VALUES (2 / 0); -- ERROR: division by zero
ROLLBACK TO SAVEPOINT my_savepoint;
INSERT INTO t_test (id) VALUES (2); -- Berhasil
COMMIT;
Setelah ROLLBACK TO SAVEPOINT
, transaksi kembali ke keadaan sebelum kesalahan, dan Anda bisa melanjutkan. Anda juga bisa menghapus savepoint dengan RELEASE SAVEPOINT
.
d. DDL Transaksional
PostgreSQL mendukung Data Definition Language (DDL) dalam transaksi, seperti CREATE TABLE
atau ALTER TABLE
. Ini berarti Anda bisa membatalkan perubahan struktur data jika diperlukan.
Contoh:
BEGIN;
CREATE TABLE t_test (id int);
ALTER TABLE t_test ADD COLUMN name text;
ROLLBACK; -- Tabel t_test tidak akan ada
Fitur ini sangat berguna untuk deployment perangkat lunak, memastikan perubahan struktur dilakukan secara atomik.
3. Apa Itu Penguncian (Locking) dan Mengapa Penting?
Penguncian adalah mekanisme untuk mengatur akses konkuren ke data, mencegah konflik antara transaksi. Tanpa penguncian yang tepat, aplikasi bisa mengalami masalah seperti:
Data tidak konsisten: Dua transaksi mengubah data yang sama secara bersamaan.
Performa buruk: Transaksi menunggu terlalu lama (waiting adalah bentuk eksekusi paling lambat).
Deadlock: Dua transaksi saling mengunci, menyebabkan kebuntuan.
Penguncian yang baik memastikan konkurensi tinggi, performa optimal, dan keandalan data.
4. Mekanisme Penguncian di PostgreSQL
a. Multi-Version Concurrency Control (MVCC)
PostgreSQL menggunakan MVCC untuk memungkinkan pembacaan dan penulisan data secara konkuren tanpa pemblokiran. Dalam MVCC:
Setiap transaksi melihat snapshot data pada saat transaksi dimulai.
Perubahan oleh transaksi lain tidak terlihat hingga di-commit.
Contoh:
BEGIN;
BEGIN;
UPDATE t_test SET id = id + 1;
SELECT * FROM t_test;
Melihat data lama (sebelum UPDATE)
COMMIT;
COMMIT;
MVCC memastikan transaksi baca tidak terblokir oleh transaksi tulis.
b. Penguncian Baris (Row-Level Locking)
PostgreSQL hanya mengunci baris yang diubah oleh transaksi, bukan seluruh tabel. Ini memungkinkan konkurensi tinggi. Contoh:
BEGIN;
BEGIN;
UPDATE t_test SET id = id + 1 WHERE id = 1;
UPDATE t_test SET id = id + 1 WHERE id = 2;
Berhasil (id = 2)
Berhasil (id = 3)
COMMIT;
COMMIT;
Karena baris yang berbeda dikunci, kedua transaksi dapat berjalan bersamaan.
c. Penguncian Tabel (Table-Level Locking)
Untuk kasus tertentu, Anda bisa mengunci seluruh tabel menggunakan perintah LOCK
. PostgreSQL mendukung beberapa mode penguncian tabel, dari yang paling ringan hingga paling ketat:
ACCESS SHARE: Diambil oleh
SELECT
, hanya konflik denganACCESS EXCLUSIVE
.ROW EXCLUSIVE: Diambil oleh
INSERT
,UPDATE
,DELETE
.ACCESS EXCLUSIVE: Mengunci tabel sepenuhnya, digunakan oleh
DROP TABLE
.
Contoh:
BEGIN;
LOCK TABLE t_test IN ACCESS EXCLUSIVE MODE;
INSERT INTO t_test VALUES (max(id) + 1);
COMMIT;
Namun, ACCESS EXCLUSIVE
sangat ketat dan harus dihindari karena memblokir semua akses lain.
d. SELECT FOR UPDATE dan FOR SHARE
SELECT FOR UPDATE: Mengunci baris yang dipilih, mencegah perubahan oleh transaksi lain hingga transaksi selesai. Contoh:
BEGIN;
SELECT * FROM invoice WHERE processed = false FOR UPDATE;
UPDATE invoice SET processed = true;
COMMIT;
SELECT FOR SHARE: Mengunci baris untuk dibaca, memungkinkan beberapa transaksi mengunci baris yang sama.
NOWAIT: Jika baris sudah terkunci, perintah gagal seketika tanpa menunggu.
SKIP LOCKED: Melewati baris yang terkunci, berguna untuk skenario seperti pemesanan tiket:
BEGIN;
SELECT * FROM t_flight LIMIT 2 FOR UPDATE SKIP LOCKED; -- Mengambil 2 baris yang tidak terkunci
COMMIT;
e. Advisory Locks
Advisory locks adalah kunci khusus yang tidak terkait dengan baris atau tabel, melainkan dengan angka tertentu. Kunci ini tidak dilepaskan otomatis saat COMMIT
, sehingga harus dilepaskan secara eksplisit.
Contoh:
BEGIN;
SELECT pg_advisory_lock(15); -- Mengunci angka 15
-- Proses
SELECT pg_advisory_unlock(15); -- Melepaskan kunci
COMMIT;
Untuk melepaskan semua advisory locks:
SELECT pg_advisory_unlock_all();
5. Tingkat Isolasi Transaksi
Tingkat isolasi menentukan bagaimana transaksi melihat perubahan oleh transaksi lain. PostgreSQL mendukung tiga tingkat:
a. Read Committed
Default: Setiap perintah dalam transaksi melihat snapshot data terbaru yang telah di-commit.
Contoh:
BEGIN;
BEGIN;
SELECT sum(balance) FROM t_account; -- 300
INSERT INTO t_account (balance) VALUES (100);
COMMIT;
SELECT sum(balance) FROM t_account; -- 400
COMMIT;
b. Repeatable Read
Menggunakan snapshot yang sama sepanjang transaksi, sehingga perubahan oleh transaksi lain tidak terlihat.
Contoh:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT sum(balance) FROM t_account; -- 300
INSERT INTO t_account (balance) VALUES (100);
COMMIT;
SELECT sum(balance) FROM t_account; -- 300
COMMIT;
c. Serializable
Memastikan transaksi berjalan seolah-olah tidak ada konkurensi, tetapi transaksi dapat gagal jika ada konflik.
Contoh:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM t_deadlock;
DELETE FROM t_deadlock; -- Gagal jika transaksi lain menghapus data
6. Menangani Masalah: Deadlock dan Konflik Serialisasi
a. Deadlock
Deadlock terjadi ketika dua transaksi saling menunggu kunci yang dipegang satu sama lain:
BEGIN;
BEGIN;
UPDATE t_deadlock SET id = id * 10 WHERE id = 1;
UPDATE t_deadlock SET id = id * 10 WHERE id = 2;
UPDATE t_deadlock SET id = id * 10 WHERE id = 2;
UPDATE t_deadlock SET id = id * 10 WHERE id = 1;
Deadlock terdeteksi, ROLLBACK
PostgreSQL mendeteksi deadlock setelah deadlock_timeout
(default 1 detik) dan membatalkan salah satu transaksi.
b. Konflik Serialisasi
Dalam mode Repeatable Read atau Serializable, transaksi dapat gagal jika mencoba mengubah data yang telah diubah oleh transaksi lain. Aplikasi harus menangani kegagalan ini dengan mencoba ulang transaksi.
7. Optimasi Penyimpanan dan Pembersihan
a. VACUUM dan Autovacuum
Karena MVCC, PostgreSQL menyimpan versi lama data hingga transaksi selesai. Proses VACUUM
membersihkan dead tuples (baris yang tidak lagi dibutuhkan) dan memperbarui free space map (FSM).
Contoh:
CREATE TABLE t_test (id int) WITH (autovacuum_enabled = off);
INSERT INTO t_test SELECT * FROM generate_series(1, 100000);
UPDATE t_test SET id = id + 1;
VACUUM t_test; -- Tidak mengubah ukuran tabel
Autovacuum menjalankan VACUUM
secara otomatis berdasarkan pengaturan seperti:
autovacuum_vacuum_scale_factor = 0.2
: Berjalan jika 20% baris berubah.autovacuum_vacuum_threshold = 50
: Minimal 50 baris berubah.
b. Transaction Wraparound
ID transaksi di PostgreSQL terbatas. Untuk mencegah wraparound, VACUUM
menyesuaikan watermark transaksi. Pengaturan penting:
autovacuum_freeze_max_age = 200000000
autovacuum_multixact_freeze_max_age = 400000000
c. VACUUM FULL
VACUUM FULL
menulis ulang tabel dan mengembalikan ruang ke sistem file, tetapi memblokir semua akses ke tabel, sehingga harus digunakan dengan hati-hati.
8. Kesimpulan
Transaksi dan penguncian di PostgreSQL dirancang untuk mendukung konkurensi tinggi dan keandalan data. Dengan fitur seperti MVCC, savepoint, DDL transaksional, dan advisory locks, Anda dapat mengelola data secara efisien. Tingkat isolasi seperti Read Committed dan Serializable memberikan fleksibilitas untuk berbagai kebutuhan aplikasi. Dengan memahami dan mengkonfigurasi VACUUM
serta autovacuum, Anda dapat menjaga performa basis data tetap optimal. Dengan pengetahuan ini, Anda dapat membangun aplikasi yang skalabel dan bebas dari masalah seperti deadlock atau konflik data.
8. Contoh Kode
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
// Koneksi ke database menggunakan pgxpool
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
// Menjalankan query sederhana sebagai transaksi
var now string
err = pool.QueryRow(ctx, "SELECT now()").Scan(&now)
if err != nil {
log.Fatalf("Gagal menjalankan query: %v", err)
}
fmt.Printf("Waktu transaksi: %s\n", now)
}
Penjelasan Kode:
Kode di atas menggunakan
pgxpool
untuk membuat koneksi ke database PostgreSQL.Query
SELECT now()
dijalankan sebagai transaksi tunggal, mengembalikan waktu transaksi.Skenario ini menunjukkan bahwa bahkan query sederhana dianggap sebagai transaksi.
2. Fitur-Fitur Transaksi
a. Perintah Dasar: BEGIN, COMMIT, ROLLBACK
BEGIN: Memulai transaksi.
COMMIT: Menyimpan perubahan.
ROLLBACK: Membatalkan perubahan.
Contoh Skenario: Aplikasi mentransfer saldo antar akun, memastikan kedua operasi berhasil atau dibatalkan.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
// Mulai transaksi
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Operasi transfer saldo
_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance - 100 WHERE user_id = 1")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal mengurangi saldo: %v", err)
}
_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance + 100 WHERE user_id = 2")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal menambah saldo: %v", err)
}
// Commit transaksi
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit transaksi: %v", err)
}
fmt.Println("Transfer berhasil!")
}
Penjelasan Kode:
Kode ini mensimulasikan transfer saldo antar dua akun.
Transaksi dimulai dengan
pool.Begin(ctx)
.Jika salah satu
UPDATE
gagal,tx.Rollback(ctx)
membatalkan semua perubahan.Jika berhasil,
tx.Commit(ctx)
menyimpan perubahan.
b. Transaction Chaining
Transaction chaining memungkinkan transaksi baru dimulai otomatis dengan properti yang sama setelah COMMIT
.
Contoh Skenario: Aplikasi mencatat log aktivitas pengguna dalam beberapa transaksi berurutan dengan mode READ ONLY
.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
// Mulai transaksi READ ONLY
tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadOnly})
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Query pertama
var count int
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM logs").Scan(&count)
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal query: %v", err)
}
fmt.Printf("Jumlah log: %d\n", count)
// Commit dengan chaining
_, err = tx.Exec(ctx, "COMMIT AND CHAIN")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal commit and chain: %v", err)
}
// Query kedua dalam transaksi baru (masih READ ONLY)
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM logs").Scan(&count)
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal query: %v", err)
}
fmt.Printf("Jumlah log (transaksi kedua): %d\n", count)
// Commit tanpa chaining
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Println("Transaksi selesai!")
}
Penjelasan Kode:
Kode ini menggunakan
BeginTx
dengan opsiReadOnly
untuk memulai transaksi.Perintah
COMMIT AND CHAIN
memulai transaksi baru dengan properti yang sama.Skenario ini mensimulasikan aplikasi yang memeriksa jumlah log berulang kali dalam mode
READ ONLY
.
c. Savepoint
Savepoint memungkinkan Anda membuat titik simpan dalam transaksi untuk kembali jika terjadi kesalahan.
Contoh Skenario: Aplikasi mencoba memperbarui data pengguna, tetapi jika gagal, kembali ke titik sebelumnya tanpa membatalkan seluruh transaksi.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Insert data pertama
_, err = tx.Exec(ctx, "INSERT INTO users (name) VALUES ('Alice')")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal insert: %v", err)
}
// Buat savepoint
_, err = tx.Exec(ctx, "SAVEPOINT my_savepoint")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal membuat savepoint: %v", err)
}
// Coba insert yang akan gagal
_, err = tx.Exec(ctx, "INSERT INTO users (id) VALUES (1/0)") // Error: division by zero
if err != nil {
// Kembali ke savepoint
_, err = tx.Exec(ctx, "ROLLBACK TO SAVEPOINT my_savepoint")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal rollback ke savepoint: %v", err)
}
}
// Insert data setelah rollback ke savepoint
_, err = tx.Exec(ctx, "INSERT INTO users (name) VALUES ('Bob')")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal insert: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Println("Transaksi berhasil, data Alice dan Bob tersimpan!")
}
Penjelasan Kode:
Kode ini mensimulasikan aplikasi yang mencoba menyimpan data pengguna.
Jika operasi gagal (misalnya, pembagian dengan nol), transaksi kembali ke
SAVEPOINT
dan melanjutkan dengan operasi lain.Data
Alice
danBob
tetap tersimpan setelahCOMMIT
.
d. DDL Transaksional
PostgreSQL mendukung perintah DDL (seperti CREATE TABLE
) dalam transaksi, yang dapat dibatalkan dengan ROLLBACK
.
Contoh Skenario: Aplikasi mencoba membuat tabel sementara untuk pengujian, tetapi membatalkannya jika tidak diperlukan.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Buat tabel sementara
_, err = tx.Exec(ctx, "CREATE TABLE temp_test (id int, name text)")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal membuat tabel: %v", err)
}
// Tambahkan kolom
_, err = tx.Exec(ctx, "ALTER TABLE temp_test ADD COLUMN age int")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal mengubah tabel: %v", err)
}
// Simulasi keputusan untuk membatalkan
fmt.Println("Membatalkan pembuatan tabel...")
err = tx.Rollback(ctx)
if err != nil {
log.Fatalf("Gagal rollback: %v", err)
}
// Verifikasi tabel tidak ada
var exists bool
err = pool.QueryRow(ctx, "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'temp_test')").Scan(&exists)
if err != nil {
log.Fatalf("Gagal memeriksa tabel: %v", err)
}
if !exists {
fmt.Println("Tabel temp_test tidak ada, rollback berhasil!")
}
}
Penjelasan Kode:
Kode ini mensimulasikan pembuatan tabel sementara dalam transaksi.
Jika aplikasi memutuskan untuk tidak menggunakan tabel,
ROLLBACK
menghapus tabel tersebut.Query verifikasi memastikan tabel tidak ada setelah
ROLLBACK
.
3. Apa Itu Penguncian (Locking) dan Mengapa Penting?
Penguncian mengatur akses konkuren ke data untuk mencegah konflik, seperti data tidak konsisten atau deadlock. Penguncian yang tepat meningkatkan performa dan keandalan aplikasi.
4. Mekanisme Penguncian di PostgreSQL
a. Multi-Version Concurrency Control (MVCC)
MVCC memungkinkan pembacaan dan penulisan data secara konkuren dengan menyimpan snapshot data untuk setiap transaksi.
Contoh Skenario: Aplikasi membaca saldo akun sementara transaksi lain memperbarui saldo.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Update saldo
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE accounts SET balance = balance + 100 WHERE user_id = 1")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update saldo: %v", err)
return
}
time.Sleep(2 * time.Second) // Simulasi proses lama
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
fmt.Println("Transaksi 1 selesai: Saldo diperbarui")
}()
// Transaksi 2: Baca saldo
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
var balance int
err = tx.QueryRow(ctx, "SELECT balance FROM accounts WHERE user_id = 1").Scan(&balance)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca saldo: %v", err)
return
}
fmt.Printf("Transaksi 2: Saldo = %d (data lama)\n", balance)
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
}()
wg.Wait()
}
Penjelasan Kode:
Dua goroutine mensimulasikan dua transaksi konkuren.
Transaksi 1 memperbarui saldo, sedangkan Transaksi 2 membaca saldo.
Karena MVCC, Transaksi 2 melihat data lama hingga Transaksi 1 di-commit.
b. Penguncian Baris (Row-Level Locking)
PostgreSQL hanya mengunci baris yang diubah, memungkinkan konkurensi tinggi.
Contoh Skenario: Dua transaksi memperbarui baris berbeda dalam tabel yang sama.
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Update baris id = 1
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE t_test SET id = id + 1 WHERE id = 1")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update baris 1: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
fmt.Println("Transaksi 1: Baris id = 1 diperbarui")
}()
// Transaksi 2: Update baris id = 2
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE t_test SET id = id + 1 WHERE id = 2")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update baris 2: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
fmt.Println("Transaksi 2: Baris id = 2 diperbarui")
}()
wg.Wait()
}
Penjelasan Kode:
Dua transaksi memperbarui baris berbeda (
id = 1
danid = 2
) secara konkuren.Karena hanya baris yang diubah yang dikunci, kedua transaksi dapat berjalan tanpa konflik.
c. Penguncian Tabel (Table-Level Locking)
Perintah LOCK
mengunci seluruh tabel, berguna untuk operasi kritis seperti pembuatan ID unik.
Contoh Skenario: Aplikasi menghasilkan nomor faktur unik tanpa duplikasi.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Kunci tabel
_, err = tx.Exec(ctx, "LOCK TABLE invoice IN ACCESS EXCLUSIVE MODE")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal mengunci tabel: %v", err)
}
// Ambil ID maksimum dan insert nomor faktur baru
var maxID int
err = tx.QueryRow(ctx, "SELECT COALESCE(MAX(id), 0) FROM invoice").Scan(&maxID)
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal mendapatkan max ID: %v", err)
}
_, err = tx.Exec(ctx, "INSERT INTO invoice (id, description) VALUES ($1, $2)", maxID+1, "Faktur Baru")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal insert faktur: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Printf("Faktur baru dengan ID %d berhasil dibuat\n", maxID+1)
}
Penjelasan Kode:
Kode ini mengunci tabel
invoice
denganACCESS EXCLUSIVE MODE
untuk mencegah duplikasi ID.ID maksimum diambil, lalu nomor faktur baru dimasukkan.
Penguncian tabel memastikan tidak ada transaksi lain yang mengganggu.
d. SELECT FOR UPDATE dan FOR SHARE
SELECT FOR UPDATE
mengunci baris untuk mencegah perubahan oleh transaksi lain. FOR SHARE
memungkinkan pembacaan konkuren.
Contoh Skenario: Aplikasi memproses pesanan yang belum diproses, memastikan tidak ada duplikasi pemrosesan.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Ambil pesanan yang belum diproses dengan FOR UPDATE
var orderID int
err = tx.QueryRow(ctx, "SELECT id FROM orders WHERE processed = false LIMIT 1 FOR UPDATE").Scan(&orderID)
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal mengambil pesanan: %v", err)
}
// Proses pesanan
_, err = tx.Exec(ctx, "UPDATE orders SET processed = true WHERE id = $1", orderID)
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal memproses pesanan: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Printf("Pesanan ID %d berhasil diproses\n", orderID)
}
Penjelasan Kode:
Kode ini mengambil pesanan yang belum diproses dengan
FOR UPDATE
, mengunci baris tersebut.Baris diperbarui untuk menandakan pemrosesan selesai.
FOR UPDATE
mencegah transaksi lain memproses pesanan yang sama.
e. SKIP LOCKED
SKIP LOCKED
memungkinkan transaksi melewati baris yang sudah terkunci.
Contoh Skenario: Aplikasi pemesanan tiket mengambil kursi yang tersedia tanpa menunggu kursi yang terkunci.
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Ambil dua kursi
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
var seatID int
err = tx.QueryRow(ctx, "SELECT id FROM seats LIMIT 2 FOR UPDATE SKIP LOCKED").Scan(&seatID)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal mengambil kursi: %v", err)
return
}
fmt.Printf("Transaksi 1: Kursi ID %d diambil\n", seatID)
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
}()
// Transaksi 2: Ambil dua kursi lain
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
var seatID int
err = tx.QueryRow(ctx, "SELECT id FROM seats LIMIT 2 FOR UPDATE SKIP LOCKED").Scan(&seatID)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal mengambil kursi: %v", err)
return
}
fmt.Printf("Transaksi 2: Kursi ID %d diambil\n", seatID)
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
}()
wg.Wait()
}
Penjelasan Kode:
Dua transaksi mencoba mengambil kursi dengan
FOR UPDATE SKIP LOCKED
.SKIP LOCKED
memastikan transaksi kedua mengambil kursi yang berbeda tanpa menunggu.
f. Advisory Locks
Advisory locks mengunci angka tertentu, bukan baris atau tabel, dan tidak dilepaskan otomatis saat COMMIT
.
Contoh Skenario: Aplikasi mengunci proses pembuatan laporan untuk mencegah duplikasi.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Ambil advisory lock
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
_, err = tx.Exec(ctx, "SELECT pg_advisory_lock(15)")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal mengambil advisory lock: %v", err)
return
}
fmt.Println("Transaksi 1: Advisory lock 15 diambil")
time.Sleep(2 * time.Second) // Simulasi proses laporan
_, err = tx.Exec(ctx, "SELECT pg_advisory_unlock(15)")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal melepaskan advisory lock: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
fmt.Println("Transaksi 1: Advisory lock dilepaskan")
}()
// Transaksi 2: Coba ambil advisory lock
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond) // Tunggu sebentar
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "SELECT pg_advisory_lock(15)")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal mengambil advisory lock: %v", err)
return
}
fmt.Println("Transaksi 2: Advisory lock 15 diambil")
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
}()
wg.Wait()
}
Penjelasan Kode:
Transaksi 1 mengambil advisory lock pada angka 15, mensimulasikan proses laporan.
Transaksi 2 menunggu hingga lock dilepaskan sebelum melanjutkan.
pg_advisory_unlock
memastikan lock dilepaskan secara eksplisit.
5. Tingkat Isolasi Transaksi
a. Read Committed
Mode default di mana setiap perintah melihat snapshot data terbaru yang telah di-commit.
Contoh Skenario: Aplikasi memeriksa saldo akun selama transaksi lain menambahkan dana.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Baca saldo
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
var balance int
err = tx.QueryRow(ctx, "SELECT SUM(balance) FROM accounts").Scan(&balance)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca saldo: %v", err)
return
}
fmt.Printf("Transaksi 1: Saldo awal = %d\n", balance)
time.Sleep(2 * time.Second)
err = tx.QueryRow(ctx, "SELECT SUM(balance) FROM accounts").Scan(&balance)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca saldo kedua: %v", err)
return
}
fmt.Printf("Transaksi 1: Saldo akhir = %d\n", balance)
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
}()
// Transaksi 2: Tambah saldo
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "INSERT INTO accounts (balance) VALUES (100)")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal insert saldo: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
fmt.Println("Transaksi 2: Saldo baru ditambahkan")
}()
wg.Wait()
}
Penjelasan Kode:
Transaksi 1 membaca saldo dua kali dalam mode Read Committed.
Transaksi 2 menambahkan saldo baru di tengah transaksi 1.
Transaksi 1 melihat saldo baru pada pembacaan kedua karena mode Read Committed.
b. Repeatable Read
Menggunakan snapshot yang sama sepanjang transaksi, mengabaikan perubahan oleh transaksi lain.
Contoh Skenario: Aplikasi membuat laporan saldo yang konsisten.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Baca saldo dengan REPEATABLE READ
go func() {
defer wg.Done()
tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead})
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
var balance int
err = tx.QueryRow(ctx, "SELECT SUM(balance) FROM accounts").Scan(&balance)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca saldo: %v", err)
return
}
fmt.Printf("Transaksi 1: Saldo awal = %d\n", balance)
time.Sleep(2 * time.Second)
err = tx.QueryRow(ctx, "SELECT SUM(balance) FROM accounts").Scan(&balance)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca saldo kedua: %v", err)
return
}
fmt.Printf("Transaksi 1: Saldo akhir = %d (sama karena REPEATABLE READ)\n", balance)
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
}()
// Transaksi 2: Tambah saldo
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "INSERT INTO accounts (balance) VALUES (100)")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal insert saldo: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
fmt.Println("Transaksi 2: Saldo baru ditambahkan")
}()
wg.Wait()
}
Penjelasan Kode:
Transaksi 1 menggunakan
RepeatableRead
untuk memastikan saldo yang dibaca konsisten.Meskipun Transaksi 2 menambahkan saldo, Transaksi 1 tidak melihat perubahan tersebut.
c. Serializable
Memastikan transaksi berjalan seolah-olah tidak ada konkurensi, tetapi dapat gagal jika ada konflik.
Contoh Skenario: Aplikasi mencoba menghapus data dengan isolasi Serializable.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Baca dan hapus dengan SERIALIZABLE
go func() {
defer wg.Done()
tx, err := pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
var count int
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM t_deadlock").Scan(&count)
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal baca data: %v", err)
return
}
fmt.Printf("Transaksi 1: Jumlah baris = %d\n", count)
time.Sleep(2 * time.Second)
_, err = tx.Exec(ctx, "DELETE FROM t_deadlock")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal hapus data: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v (mungkin konflik serialisasi)", err)
} else {
fmt.Println("Transaksi 1: Data dihapus")
}
}()
// Transaksi 2: Hapus data
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "DELETE FROM t_deadlock")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal hapus data: %v", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
fmt.Println("Transaksi 2: Data dihapus")
}()
wg.Wait()
}
Penjelasan Kode:
Transaksi 1 menggunakan mode Serializable dan mencoba menghapus data.
Jika Transaksi 2 menghapus data terlebih dahulu, Transaksi 1 akan gagal karena konflik serialisasi.
Aplikasi harus menangani kegagalan ini dengan mencoba ulang.
6. Menangani Masalah: Deadlock dan Konflik Serialisasi
a. Deadlock
Deadlock terjadi ketika dua transaksi saling menunggu kunci.
Contoh Skenario: Dua transaksi mencoba memperbarui dua baris dalam urutan berbeda.
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
var wg sync.WaitGroup
wg.Add(2)
// Transaksi 1: Update id = 1 lalu id = 2
go func() {
defer wg.Done()
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 1: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE t_deadlock SET id = id * 10 WHERE id = 1")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update id = 1: %v", err)
return
}
time.Sleep(1 * time.Second)
_, err = tx.Exec(ctx, "UPDATE t_deadlock SET id = id * 10 WHERE id = 2")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update id = 2: %v (mungkin deadlock)", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 1: %v", err)
}
fmt.Println("Transaksi 1 selesai")
}()
// Transaksi 2: Update id = 2 lalu id = 1
go func() {
defer wg.Done()
time.Sleep(500 * time.Millisecond)
tx, err := pool.Begin(ctx)
if err != nil {
log.Printf("Gagal memulai transaksi 2: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE t_deadlock SET id = id * 10 WHERE id = 2")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update id = 2: %v", err)
return
}
_, err = tx.Exec(ctx, "UPDATE t_deadlock SET id = id * 10 WHERE id = 1")
if err != nil {
tx.Rollback(ctx)
log.Printf("Gagal update id = 1: %v (mungkin deadlock)", err)
return
}
err = tx.Commit(ctx)
if err != nil {
log.Printf("Gagal commit transaksi 2: %v", err)
}
fmt.Println("Transaksi 2 selesai")
}()
wg.Wait()
}
Penjelasan Kode:
Dua transaksi mencoba memperbarui baris dalam urutan berbeda, menyebabkan deadlock.
PostgreSQL akan membatalkan salah satu transaksi setelah
deadlock_timeout
.
b. Konflik Serialisasi
Konflik serialisasi terjadi di mode Serializable ketika transaksi tidak dapat diserialkan.
Catatan: Contoh untuk konflik serialisasi sudah disertakan pada bagian Serializable di atas.
7. Optimasi Penyimpanan dan Pembersihan
a. VACUUM dan Autovacuum
VACUUM
membersihkan dead tuples yang dihasilkan oleh MVCC. Autovacuum menjalankannya secara otomatis.
Contoh Skenario: Aplikasi menjalankan VACUUM
secara manual setelah pembaruan besar.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Update besar
_, err = tx.Exec(ctx, "UPDATE t_test SET id = id + 1")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal update: %v", err)
}
// Jalankan VACUUM
_, err = tx.Exec(ctx, "VACUUM t_test")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal menjalankan VACUUM: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Println("VACUUM selesai, ruang kosong dikelola")
}
Penjelasan Kode:
Kode ini mensimulasikan pembaruan besar pada tabel, diikuti oleh
VACUUM
untuk membersihkan dead tuples.VACUUM
memastikan ruang kosong dapat digunakan kembali.
b. Transaction Wraparound
Autovacuum mencegah wraparound ID transaksi dengan menyesuaikan watermark.
Catatan: Tidak ada kode Golang spesifik untuk wraparound karena dikelola oleh autovacuum. Konfigurasi dilakukan di postgresql.conf
.
c. VACUUM FULL
VACUUM FULL
menulis ulang tabel untuk mengembalikan ruang ke sistem file, tetapi memblokir akses.
Contoh Skenario: Aplikasi menjalankan VACUUM FULL
pada tabel kecil untuk pengujian.
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:password@localhost:5432/testdb")
if err != nil {
log.Fatalf("Gagal koneksi ke database: %v", err)
}
defer pool.Close()
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatalf("Gagal memulai transaksi: %v", err)
}
// Jalankan VACUUM FULL
_, err = tx.Exec(ctx, "VACUUM FULL t_test")
if err != nil {
tx.Rollback(ctx)
log.Fatalf("Gagal menjalankan VACUUM FULL: %v", err)
}
err = tx.Commit(ctx)
if err != nil {
log.Fatalf("Gagal commit: %v", err)
}
fmt.Println("VACUUM FULL selesai, tabel ditulis ulang")
}
Penjelasan Kode:
Kode ini menjalankan
VACUUM FULL
untuk menulis ulang tabel.Digunakan untuk tabel kecil karena mengunci tabel sepenuhnya.
8. Kesimpulan
Artikel ini telah menjelaskan transaksi dan penguncian di PostgreSQL dengan contoh kode Golang menggunakan pgxpool
. Fitur seperti MVCC, savepoint, DDL transaksional, dan advisory locks memungkinkan pengelolaan data yang efisien dan konkuren. Tingkat isolasi (Read Committed, Repeatable Read, Serializable) memberikan fleksibilitas untuk berbagai kebutuhan aplikasi. Dengan VACUUM
dan autovacuum, Anda dapat menjaga performa basis data. Kode-kode di atas mensimulasikan skenario nyata seperti transfer saldo, pemesanan tiket, dan pembuatan laporan, membantu Anda memahami penerapan praktis dari konsep ini.
Catatan Tambahan:
Pastikan Anda mengganti string koneksi (
postgres://user:password@localhost:5432/testdb
) dengan kredensial database Anda.Tabel seperti
accounts
,t_test
,invoice
,orders
,seats
, dant_deadlock
harus dibuat terlebih dahulu di database untuk menjalankan kode.Kode menggunakan
sync.WaitGroup
untuk mensimulasikan konkurensi dalam skenario seperti MVCC, deadlock, dan SKIP LOCKED.Library
pgxpool
dipilih karena mendukung koneksi pool yang efisien untuk aplikasi produksi.
Jika Anda memerlukan bantuan untuk membuat tabel atau menjalankan kode, beri tahu saya!
Last updated