Transactions & Isolation Levels — Database

Apa itu Transaction? Transaction adalah sekelompok operasi database yang dijalankan sebagai satu unit. Semua berhasil, atau semua dibatalkan (all-or-nothing)…

Transaction adalah sekelompok perintah database yang jadi satu paket: semuanya berhasil, atau semuanya dibatalkan. Isolation level mengatur hal lain: kalau dua transaksi jalan bersamaan, seberapa banyak satu transaksi boleh melihat pekerjaan transaksi lainnya. Bayangin dua kasir Kedai Senja yang mencatat stok di satu buku yang sama. Kalau keduanya membaca angka yang sama lalu sama-sama menulis ulang, salah satu catatan bisa tertimpa. Isolation level adalah aturan pakai buku itu bersama.

🧪 Tebakan cepat: stok Kopi Susu tinggal 10 cup. Kasir A dan kasir B masing-masing membaca stok (10), lalu menulis 10 dikurangi 1, yaitu 9, dan COMMIT. Dua cup terjual. Stok akhirnya berapa: 8, 9, atau error? Tebak dulu, jawabannya ada di bagian "Lost update".

Transaksi dan ACID

BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = 1;
INSERT INTO orders (customer_id, total) VALUES (1, 18000);
COMMIT;   -- atau ROLLBACK; untuk membatalkan semuanya

Transaksi yang baik punya empat sifat, disingkat ACID:

Di Laravel, DB::transaction(function () { ... }) menjalankan isinya dalam satu transaksi: COMMIT otomatis kalau selesai, ROLLBACK otomatis kalau ada exception.

Empat masalah saat transaksi jalan bersamaan

Masalah Yang terjadi Contoh di Kedai Senja
Dirty read membaca perubahan transaksi lain yang belum di-commit A mengubah stok jadi 5 lalu ROLLBACK, tapi B sempat membaca 5
Non-repeatable read membaca baris yang sama dua kali, hasilnya beda B membaca stok 10, A mengubahnya jadi 5 dan COMMIT, B membaca lagi: 5
Phantom read query yang sama mengembalikan jumlah baris yang beda B menghitung pesanan Budi: 2. A menambah satu dan COMMIT. B menghitung lagi: 3
Lost update dua transaksi membaca nilai lama lalu sama-sama menimpa dua cup terjual, stok cuma turun 1

Lost update

Ini urutan kejadian di tebakan tadi, dengan isolation level bawaan PostgreSQL (READ COMMITTED):

Langkah Kasir A Kasir B stock di database
1 SELECT stock dapat 10 10
2 SELECT stock dapat 10 10
3 UPDATE ... SET stock = 9 10, perubahan A belum di-commit
4 COMMIT 9
5 UPDATE ... SET stock = 9 9
6 COMMIT 9

Stok akhirnya 9, tanpa error. Itu jawaban tebakannya. Dua cup terjual, satu pengurangan hilang, karena B menghitung 9 dari angka 10 yang sudah basi.

Obatnya, jangan hitung di aplikasi. Biarkan database yang mengurangi:

UPDATE products SET stock = stock - 1 WHERE id = 1;

Di READ COMMITTED, UPDATE ini mengunci barisnya, dan transaksi kedua menunggu sampai yang pertama selesai, lalu mengurangi dari angka terbaru: stok jadi 8. Kalau aplikasi perlu membaca dulu (misalnya mengecek stok cukup), kunci barisnya saat membaca dengan SELECT stock FROM products WHERE id = 1 FOR UPDATE;.

Empat isolation level

Standar SQL mendefinisikan empat level, dari yang paling longgar ke paling ketat. Makin ketat, makin sedikit masalah, tapi makin sering transaksi saling menunggu atau dibatalkan.

Level Dirty read Non-repeatable read Phantom read
READ UNCOMMITTED bisa bisa bisa
READ COMMITTED dicegah bisa bisa
REPEATABLE READ dicegah dicegah bisa
SERIALIZABLE dicegah dicegah dicegah

SERIALIZABLE paling ketat: hasilnya dijamin sama dengan kalau transaksi-transaksinya dijalankan satu per satu, berurutan. Dirty read cuma mungkin di READ UNCOMMITTED.

PostgreSQL dan MySQL gak sama

Tabel di atas adalah batas minimum dari standar. Tiap database mengisinya dengan caranya sendiri:

Level PostgreSQL MySQL (InnoDB)
READ UNCOMMITTED diperlakukan sama dengan READ COMMITTED: dirty read gak pernah terjadi dirty read bisa terjadi
READ COMMITTED default. Tiap SELECT melihat data yang sudah di-commit saat itu sama
REPEATABLE READ satu snapshot untuk seluruh transaksi, jadi phantom juga gak terjadi. Lost update dihentikan dengan ERROR: could not serialize access due to concurrent update default. SELECT biasa membaca snapshot, tapi UPDATE membaca angka terbaru: lost update tetap lolos
SERIALIZABLE transaksi jalan tanpa saling menunggu, lalu salah satunya dibatalkan dengan error kalau hasilnya gak mungkin dicapai secara berurutan SELECT biasa berubah jadi SELECT ... FOR SHARE: transaksi saling menunggu kunci, kadang berakhir Deadlock found when trying to get lock

SQLite lebih sederhana: cuma satu transaksi yang boleh menulis dalam satu waktu. Penulis kedua menunggu, atau mendapat error database is locked. Hasilnya selalu serializable.

Di semua database, transaksi yang dibatalkan karena konflik (serialization error atau deadlock) harus diulang oleh aplikasi. Di Laravel, angka kedua DB::transaction() adalah jumlah percobaan:

DB::transaction(function () {
    DB::table('products')->where('id', 1)->decrement('stock'); // stock = stock - 1
    DB::table('orders')->insert(['customer_id' => 1, 'total' => 18000]);
}, 3); // ulangi sampai 3 kali kalau kena deadlock atau serialization error

Memilih level

Kebutuhan Level
kecepatan maksimal read uncommitted, paling longgar. Jarang dipakai, dan di PostgreSQL sama saja dengan read committed
default, umum read committed, default PostgreSQL dan pilihan aman untuk kebanyakan aplikasi
konsistensi baca repeatable read, misalnya laporan yang membaca banyak tabel dan harus konsisten satu sama lain
keamanan maksimal, financial serializable, untuk uang dan stok yang gak boleh salah, dengan percobaan ulang di aplikasi

Cara menyetelnya: di PostgreSQL BEGIN ISOLATION LEVEL SERIALIZABLE;, di MySQL SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; sebelum START TRANSACTION;.

⚠️ Jebakan yang sering ketemu

TL;DR: Transaksi menjalankan perintah sebagai satu paket (ACID), dan DB::transaction() membungkusnya di Laravel. Saat transaksi jalan bersamaan, ada empat masalah: dirty read (membaca yang belum di-commit), non-repeatable read (baris yang sama berubah), phantom read (jumlah baris berubah), dan lost update (dua penulis saling menimpa, stok 10 dikurangi dua kali jadi 9). Empat level dari longgar ke ketat: READ UNCOMMITTED, READ COMMITTED (default PostgreSQL), REPEATABLE READ (default MySQL) dan SERIALIZABLE (paling ketat, seolah berurutan). PostgreSQL dan MySQL berbeda di level yang sama, SQLite cuma punya satu penulis. Cegah lost update dengan stock = stock - 1 atau FOR UPDATE, dan ulangi transaksi yang dibatalkan karena konflik.

Yang akan kamu pelajari