Skip to content

ACID di Database: Empat Jaminan yang Membuat Transaction Bisa Dipercaya ​

Bayangkan kamu sedang membangun fitur transfer saldo antar pengguna di aplikasi perbankan atau dompet digital.

Pengguna A mengirimkan saldo sebesar Rp100.000 ke Pengguna B. Proses ini membutuhkan setidaknya dua operasi data:

  1. Mengurangi saldo Pengguna A sebesar Rp100.000.
  2. Menambah saldo Pengguna B sebesar Rp100.000.

Jika kedua operasi tersebut dijalankan sebagai dua query SQL terpisah tanpa pengaman transaction, masalah serius bisa terjadi kapan saja. Misalkan server aplikasi atau database mendadak mati tepat setelah query pertama selesai dieksekusi. Saldo Pengguna A sudah berkurang, tetapi saldo Pengguna B belum bertambah. Uang tersebut hilang dari pencatatan sistem.

Situasi seperti ini adalah alasan utama kita membutuhkan transaction dan jaminan ACID di database.

Apa itu transaction? ​

Transaction adalah satu unit kerja logis yang terdiri dari satu atau beberapa perintah SQL. Karakteristik utama dari transaction adalah sifat keutuhannya: seluruh perintah di dalamnya harus selesai bersamaan, atau dibatalkan seluruhnya seolah-olah tidak pernah terjadi.

Di database SQL standar, transaction dikendalikan menggunakan tiga perintah utama:

  • BEGIN (atau START TRANSACTION): menandai awal dari rangkaian operasi dalam transaction.
  • COMMIT: menyimpan seluruh perubahan data secara permanen ke database.
  • ROLLBACK: membatalkan seluruh operasi yang sempat dijalankan sejak perintah BEGIN, mengembalikan data ke kondisi semula.

Konsep Dasar

ACID adalah singkatan dari empat jaminan inti yang disediakan oleh database relasional pada setiap transaction: Atomicity, Consistency, Isolation, dan Durability. Keempat jaminan ini memastikan integrity data tetap terjaga meskipun terjadi kegagalan sistem, crash hardware, atau akses bersamaan dari banyak pengguna.

Loading diagram...

Atomicity: Semua berhasil atau tidak sama sekali ​

Atomicity memastikan bahwa seluruh statement di dalam satu transaction diperlakukan sebagai satu kesatuan yang tidak dapat dipecah. Jika salah satu statement gagal dijalankan atau terjadi error sebelum COMMIT, database secara otomatis membatalkan seluruh perubahan yang sudah dilakukan sebelumnya dalam transaction tersebut.

Contoh transaction transfer saldo dengan penanganan rollback:

sql
BEGIN;

-- Langkah 1: Kurangi saldo pengirim
UPDATE accounts
SET balance = balance - 100000
WHERE account_id = 'ACC-001';

-- Langkah 2: Tambah saldo penerima
UPDATE accounts
SET balance = balance + 100000
WHERE account_id = 'ACC-002';

-- Jika kedua langkah selesai tanpa error, simpan permanen
COMMIT;

Jika terjadi gangguan koneksi network atau error syntax pada langkah kedua, aplikasi atau database akan memanggil ROLLBACK. Pengurangan saldo pada Pengguna A yang sudah sempat terjadi di memori kerja database akan dibatalkan seketika. Saldo Pengguna A kembali utuh seperti sebelum transaction dimulai.

Consistency: Menjaga aturan data tetap valid ​

Consistency menjamin bahwa transaction hanya membawa database dari satu kondisi valid ke kondisi valid lainnya. Setiap perubahan data wajib memenuhi seluruh aturan, integrity constraint, dan skema yang telah didefinisikan pada tabel.

Aturan yang dijaga oleh database meliputi:

  • NOT NULL: kolom wajib memiliki nilai.
  • UNIQUE: nilai kolom tidak boleh duplikat, seperti email atau nomor rekening.
  • CHECK constraint: nilai kolom harus memenuhi ekspresi tertentu, misalnya CHECK (balance >= 0).
  • FOREIGN KEY: relasi antar tabel harus selalu valid dan menunjuk data yang memang ada.

Contoh skema dengan constraint pengaman:

sql
CREATE TABLE accounts (
  account_id VARCHAR(32) PRIMARY KEY,
  owner_name VARCHAR(100) NOT NULL,
  balance NUMERIC(12, 2) NOT NULL,
  CONSTRAINT positive_balance CHECK (balance >= 0)
);

Jika Pengguna A hanya memiliki saldo Rp50.000 dan mencoba mentransfer Rp100.000, eksekusi query pengurangan saldo akan menghasilkan nilai negatif (-50000). Database mendeteksi pelanggaran batasan CHECK (balance >= 0) dan menolak query tersebut dengan pesan error. Transaction langsung digagalkan sebelum data yang melanggar aturan sempat tersimpan.

Database hanya menegakkan aturan yang telah dideklarasikan secara eksplisit dalam skema tabel. Aturan bisnis tingkat tinggi yang belum dituangkan ke dalam constraint database tetap menjadi tanggung jawab logika kode aplikasi kamu.

Isolation: Menangani transaction yang berjalan bersamaan ​

Di lingkungan production, ribuan transaction dapat berjalan pada detik yang sama. Jaminan Isolation mengatur agar eksekusi transaction yang berjalan bersamaan (concurrent) tidak saling merusak atau menghasilkan data yang keliru.

Masalah lost update ​

Masalah concurrency yang paling umum di pekerjaan adalah lost update. Bayangkan dua proses kasir membaca saldo produk yang sama secara simultan:

  1. Transaction 1 membaca stok barang: 10 unit.
  2. Transaction 2 membaca stok barang yang sama: 10 unit.
  3. Transaction 1 menjual 1 unit, menghitung 10 - 1 = 9, lalu menyimpan angka 9 ke database.
  4. Transaction 2 menjual 2 unit, menghitung 10 - 2 = 8, lalu menimpa baris yang sama dengan angka 8.

Hasil akhir stok menjadi 8 unit, padahal total barang yang terjual adalah 3 unit (stok yang benar seharusnya 7 unit). Perubahan dari Transaction 1 tertimpa dan hilang karena kedua transaction membaca data awal yang sama secara bersamaan.

Isolation level default di database populer ​

Database menyediakan beberapa transaction isolation level dengan trade-off antara keamanan data dan performa throughput:

  • PostgreSQL: level default adalah READ COMMITTED. Satu transaction hanya dapat membaca data yang sudah di-commit oleh transaction lain.
  • MySQL (InnoDB): level default adalah REPEATABLE READ. Pembacaan data di dalam transaction bersifat konsisten terhadap snapshot data saat transaction pertama kali membaca record tersebut.

Row locking dengan SELECT FOR UPDATE ​

Untuk mencegah masalah lost update pada transaction kritis seperti mutasi saldo atau stok, kamu dapat menggunakan row locking (pessimistic locking):

sql
BEGIN;

-- Lock baris akun pengirim dan penerima agar transaction lain harus menunggu giliran
SELECT balance
FROM accounts
WHERE account_id IN ('ACC-001', 'ACC-002')
ORDER BY account_id
FOR UPDATE;

-- Jalankan pembaruan saldo dengan aman
UPDATE accounts
SET balance = balance - 100000
WHERE account_id = 'ACC-001';

UPDATE accounts
SET balance = balance + 100000
WHERE account_id = 'ACC-002';

COMMIT;

Klausa FOR UPDATE mengunci record ACC-001 dan ACC-002 sampai transaction pemanggil melakukan COMMIT atau ROLLBACK. Transaction lain yang mencoba mengakses atau memodifikasi salah satu baris tersebut akan mengantre dengan aman.

Perhatikan tambahan ORDER BY account_id sebelum FOR UPDATE. Baris dikunci secara berurutan sesuai urutan hasil query. Tanpa urutan yang konsisten, dua transaction yang mentransfer saldo secara berlawanan arah (A ke B, dan B ke A) bisa saling mengunci baris berbeda di waktu yang hampir bersamaan, lalu saling menunggu tanpa akhir. Kondisi ini disebut deadlock. Selalu lock beberapa baris dalam urutan yang konsisten, misalnya berdasarkan account_id atau id.

Bacaan lanjutan

Untuk memahami bagaimana deadlock terjadi langkah demi langkah, bagaimana database mendeteksi dan memilih victim, serta contoh kasus lain di batch worker, baca article Database Deadlock: Ketika Dua Transaction Saling Menunggu Tanpa Akhir.

Durability: Data tersimpan aman setelah commit ​

Durability memastikan bahwa sekali transaction dinyatakan berhasil melalui konfirmasi COMMIT, seluruh perubahan data akan tersimpan permanen dan bertahan meskipun server database mendadak mati, restart, atau mengalami kegagalan pasokan daya listrik.

Peran Write-Ahead Logging (WAL) ​

Menulis seluruh halaman data tabel ke disk secara acak pada setiap transaction membutuhkan waktu yang lama. Untuk menjaga kecepatan transaction sekaligus mempertahankan durability, database modern memanfaatkan mekanisme Write-Ahead Logging (WAL di PostgreSQL, atau redo log di MySQL):

  1. Saat transaction melakukan commit, database terlebih dahulu mencatat perubahan transaction ke dalam file log berurutan (append-only) di disk.
  2. Setelah catatan log tersimpan aman ke media penyimpanan, database mengonfirmasi ke aplikasi bahwa transaction telah berhasil (COMMIT).
  3. Penulisan aktual ke file tabel utama dapat dilakukan secara berkala di latar belakang.

Jika server mati mendadak beberapa detik setelah commit, proses database akan membaca kembali file log WAL saat booting untuk memulihkan seluruh perubahan yang belum sempat disinkronkan ke tabel utama.

Bacaan lanjutan

Untuk memahami cara kerja WAL secara utuh, termasuk alur COMMIT dan SELECT lewat buffer cache, kapan checkpoint berjalan, dan alasan tulisan ke WAL lebih cepat daripada tulisan ke tabel utama, baca article Write-Ahead Log: Kenapa Database Menulis Dua Kali tapi Tetap Cepat.

Contoh implementasi transaction di Node.js ​

Berikut contoh kode penanganan transaction mutasi saldo menggunakan library PostgreSQL di Node.js:

javascript
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

async function transferBalance(senderId, receiverId, amount) {
  const client = await pool.connect();

  try {
    // Mulai transaction
    await client.query("BEGIN");

    // Validasi dan lock baris pengirim dan penerima untuk mencegah lost update
    const lockedRes = await client.query(
      `SELECT account_id, balance
       FROM accounts
       WHERE account_id = ANY($1::text[])
       ORDER BY account_id
       FOR UPDATE`,
      [[senderId, receiverId]],
    );

    const balances = Object.fromEntries(
      lockedRes.rows.map((row) => [row.account_id, Number(row.balance)]),
    );

    if (balances[senderId] === undefined) {
      throw new Error("Akun pengirim tidak ditemukan");
    }
    if (balances[receiverId] === undefined) {
      throw new Error("Akun penerima tidak ditemukan");
    }

    if (balances[senderId] < amount) {
      throw new Error("Saldo pengirim tidak mencukupi");
    }

    // Eksekusi mutasi saldo
    await client.query(
      "UPDATE accounts SET balance = balance - $1 WHERE account_id = $2",
      [amount, senderId],
    );

    await client.query(
      "UPDATE accounts SET balance = balance + $1 WHERE account_id = $2",
      [amount, receiverId],
    );

    // Simpan seluruh perubahan secara permanen
    await client.query("COMMIT");
    return { success: true };
  } catch (error) {
    // Batalkan seluruh perubahan jika terjadi kesalahan
    await client.query("ROLLBACK");
    throw error;
  } finally {
    // Selalu kembalikan client ke connection pool
    client.release();
  }
}

Blok try ... catch ... finally di atas memastikan tiga hal penting:

  • Perubahan disimpan permanen dengan COMMIT saat seluruh query sukses.
  • Perubahan dibatalkan dengan ROLLBACK saat terjadi kegagalan di tengah proses.
  • Koneksi selalu dikembalikan ke pool melalui client.release() agar tidak terjadi kebocoran koneksi.

Penerapan ACID di pekerjaan ​

Jaminan ACID sangat krusial pada domain sistem yang menuntut akurasi data absolut, seperti:

  • Sistem finansial, dompet digital, dan pencatatan mutasi perbankan.
  • Sistem manajemen persediaan dan reservasi inventaris e-commerce.
  • Pembukuan akuntansi dan invoicing.

Pada arsitektur sistem berskala besar atau sistem terdistribusi (microservices), mempertahankan keempat jaminan ACID di seluruh database yang terpisah sering kali menurunkan performa dan ketersediaan sistem. Sebagai kompromi, sistem terdistribusi kerap menerapkan model konsistensi bertahap (eventual consistency). Pada model ini, data di berbagai service akan menjadi konsisten setelah beberapa saat melalui mekanisme message broker atau asynchronous event.

Rangkuman ​

  • Transaction adalah kumpulan operasi data yang diperlakukan sebagai satu unit kerja utuh.
  • Atomicity memastikan seluruh perintah dalam transaction sukses bersama atau dibatalkan seluruhnya.
  • Consistency menjamin setiap transaction mematuhi skema dan batasan data yang telah dikonfigurasi.
  • Isolation mencegah transaction yang berjalan paralel saling merusak data melalui isolation level dan row locking.
  • Durability menjamin data yang sudah di-commit tetap tersimpan aman melalui mekanisme write-ahead log meskipun sistem mengalami crash.

References ​