Sejauh ini setiap perubahan data yang kita lakukan berdiri sendiri: satu statement UPDATE, satu efek permanen. Operasi bisnis di dunia nyata jarang sesederhana itu — satu transfer uang, satu pemesanan tiket, atau satu penjualan barang terdiri dari beberapa statement yang harus berhasil bersama-sama. Bila sebagian berhasil dan sebagian gagal, database berakhir dalam keadaan setengah jadi yang merusak kepercayaan terhadap seluruh sistem. Pada chapter ini kita bedah masalah klasik transfer uang dua langkah, mendefinisikan transaksi sebagai unit kerja all-or-nothing, lalu mengamati mode autocommit MySQL yang selama ini diam-diam bekerja di belakang setiap statement kita.

Masalah Transfer Uang Dua Langkah #

Kita pakai database latihan belajar_trx dengan dua tabel: rekening menyimpan saldo dua nasabah, dan mutasi mencatat setiap pergerakan uang. Kolom rekening_id pada tabel mutasi terikat foreign key ke rekening(id) supaya setiap catatan mutasi selalu menunjuk rekening yang benar-benar ada:

sql
DROP DATABASE IF EXISTS belajar_trx;
CREATE DATABASE belajar_trx;
USE belajar_trx;
CREATE TABLE rekening (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pemilik VARCHAR(50) NOT NULL,
  saldo DECIMAL(12,2) NOT NULL
);
CREATE TABLE mutasi (
  id INT AUTO_INCREMENT PRIMARY KEY,
  rekening_id INT NOT NULL,
  jumlah DECIMAL(12,2) NOT NULL,
  keterangan VARCHAR(100) NOT NULL,
  tanggal DATE NOT NULL,
  CONSTRAINT fk_mutasi_rekening FOREIGN KEY (rekening_id) REFERENCES rekening(id)
);
INSERT INTO rekening (pemilik, saldo) VALUES ('Budi', 500000.00), ('Siti', 300000.00);
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
SELECT SUM(saldo) AS total_uang FROM rekening;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

+----+---------+-----------+
| id | pemilik | saldo     |
+----+---------+-----------+
|  1 | Budi    | 500000.00 |
|  2 | Siti    | 300000.00 |
+----+---------+-----------+
2 rows in set (0.00 sec)

+------------+
| total_uang |
+------------+
|  800000.00 |
+------------+
1 row in set (0.00 sec)

Total uang dalam sistem Rp800.000,00 — Budi memegang Rp500.000,00 dan Siti memegang Rp300.000,00. Skenario kita: Budi mentransfer Rp100.000,00 kepada Siti. Operasi ini membutuhkan dua UPDATE: potong saldo Budi, lalu tambah saldo Siti. Dua statement tersebut harus dijalankan berurutan karena satu transfer adalah satu peristiwa bisnis yang tidak bisa dibelah.

Saat Aplikasi Berhenti di Tengah Jalan #

Kita jalankan langkah pertama transfer sebagaimana biasanya — sebuah UPDATE tunggal tanpa pembungkus apa pun:

sql
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
SELECT SUM(saldo) AS total_uang FROM rekening;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+----+---------+-----------+
| id | pemilik | saldo     |
+----+---------+-----------+
|  1 | Budi    | 400000.00 |
|  2 | Siti    | 300000.00 |
+----+---------+-----------+
2 rows in set (0.00 sec)

+------------+
| total_uang |
+------------+
|  700000.00 |
+------------+
1 row in set (0.00 sec)

Saldo Budi berubah menjadi Rp400.000,00 dan perubahan ini langsung tersimpan permanen. Sekarang mari kita bayangkan aplikasi transfer berhenti bekerja tepat di titik ini: server listrik mati, proses aplikasi crash, atau koneksi jaringan terputus sebelum statement kedua sempat dikirim. Keadaan database persis seperti output di atas — Rp100.000,00 sudah keluar dari rekening Budi, tetapi belum masuk ke rekening siapa pun. Total uang di sistem tinggal Rp700.000,00; seratus ribu rupiah lenyap dari peredaran tanpa jejak. Ini bukan skenario teori: pada sistem nyata, “crash di antara dua statement” terjadi setiap kali terjadi pemadaman, kehabisan memori, atau timeout jaringan.

Keadaan setengah jadi seperti inilah yang disebut pembaruan parsial (partial update). Kita bisa memulihkannya secara manual karena latihan ini masih kecil:

sql
UPDATE rekening SET saldo = 500000.00 WHERE id = 1;
SELECT SUM(saldo) AS total_uang FROM rekening;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+------------+
| total_uang |
+------------+
|  800000.00 |
+------------+
1 row in set (0.00 sec)

Saldo Budi kembali Rp500.000,00 dan total uang pulih ke Rp800.000,00. Namun pemulihan manual hanya mungkin karena kita tahu persis angka mana yang berubah. Pada bank dengan jutaan transaksi per hari, tidak ada manusia yang bisa merekonstruksi state setengah jadi mana pun yang tertinggal — yang dibutuhkan adalah mekanisme yang mencegah state setengah jadi terjadi sejak awal.

Autocommit: Setiap Statement adalah Transaksi #

Mengapa UPDATE tunggal di atas langsung permanen padahal kita tidak menjalankan perintah penyimpanan apa pun? Jawabannya ada pada variabel sesi autocommit:

sql
SELECT @@autocommit;

Output yang Dihasilkan:

text
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

Nilai 1 berarti autocommit aktif — perilaku bawaan MySQL. Dalam mode ini, setiap statement DML (INSERT, UPDATE, DELETE) diperlakukan sebagai transaksi tersendiri: MySQL membungkus statement tersebut, menjalankannya, lalu langsung meng-commit tanpa menunggu perintah dari kita. Itulah sebabnya UPDATE pertama kita seolah “menyimpan sendiri” — memang ada transaksi yang terjadi, hanya berumur satu statement.

Transaksi: Unit Kerja All-or-Nothing #

Transaksi adalah kumpulan statement yang diperlakukan server sebagai satu unit kerja dengan jaminan all-or-nothing: seluruh perubahan di dalamnya berlaku bersama-sama, atau tidak ada satu pun yang berlaku. Kita buka transaksi dengan START TRANSACTION, jalankan kedua langkah transfer di dalamnya, lalu tutup dengan COMMIT (mengukuhkan) atau ROLLBACK (membatalkan). Untuk sekadar merasakan jaminannya, kita jalankan transfer lengkap lalu batalkan:

sql
START TRANSACTION;
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;
UPDATE rekening SET saldo = saldo + 100000 WHERE id = 2;
INSERT INTO mutasi (rekening_id, jumlah, keterangan, tanggal) VALUES
  (1, -100000.00, 'Transfer ke Siti', '2026-01-15'),
  (2, 100000.00, 'Transfer dari Budi', '2026-01-15');
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
SELECT SUM(saldo) AS total_uang FROM rekening;
ROLLBACK;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
SELECT COUNT(*) AS jumlah_mutasi FROM mutasi;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

+----+---------+-----------+
| id | pemilik | saldo     |
+----+---------+-----------+
|  1 | Budi    | 400000.00 |
|  2 | Siti    | 400000.00 |
+----+---------+-----------+
2 rows in set (0.00 sec)

+------------+
| total_uang |
+------------+
|  800000.00 |
+------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----+---------+-----------+
| id | pemilik | saldo     |
+----+---------+-----------+
|  1 | Budi    | 500000.00 |
|  2 | Siti    | 300000.00 |
+----+---------+-----------+
2 rows in set (0.00 sec)

+---------------+
| jumlah_mutasi |
+---------------+
|             0 |
+---------------+
1 row in set (0.00 sec)

Mari kita baca jalannya peristiwa baris demi baris: START TRANSACTION membuka transaksi dan sejak saat itu autocommit ditangguhkan. Dua UPDATE memindahkan Rp100.000,00 dari Budi ke Siti, dua INSERT mencatat mutasinya — dan SELECT pertama menunjukkan keadaan baru itu: saldo Rp400.000,00 dan Rp400.000,00, total tetap Rp800.000,00. Lalu ROLLBACK membatalkan seluruh rangkaian sekaligus: SELECT terakhir menunjukkan saldo kembali Rp500.000,00 dan Rp300.000,00, tabel mutasi kembali kosong. Seandainya kita menulis COMMIT alih-alih ROLLBACK, kedua langkah transfer akan tersimpan permanen sebagai satu kesatuan — itu materi chapter berikutnya.

Diagram berikut merangkum siklus hidup transaksi:

Keadaan yang Dipecahkan Transaksi #

Dua kata kunci merangkum manfaat transaksi: konsistensi dan keutuhan. Tabel berikut memetakan masalah nyata ke jaminan yang diberikan transaksi:

Masalah tanpa transaksiJaminan dengan transaksi
Crash di tengah rangkaian meninggalkan data setengah jadiAtomicity: semua langkah berlaku atau tidak sama sekali
Saldo total tidak seimbang karena satu sisi transfer hilangConsistency: aturan bisnis dan constraint tetap terjaga
Pembaca lain melihat keadaan setengah jadiIsolation: pembaca hanya melihat keadaan utuh
Perubahan hilang setelah crash meski sudah diumumkan berhasilDurability: hasil COMMIT bertahan crash

Empat jaminan itu dikenal sebagai properti ACID — kita bedah satu per satu di chapter khusus. Sebelum sampai di sana, kita perlu menguasai perintah pembuka dan penutup transaksi terlebih dahulu.

Membersihkan Database Latihan #

Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:

sql
DROP DATABASE belajar_trx;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Masalah transfer dua langkah kini punya penawarnya: transaksi menjadikan beberapa statement sebagai satu unit kerja yang utuh, dan kita sudah membuktikan ROLLBACK mengembalikan seluruh rangkaian transfer dalam satu ketukan. Namun contoh kita masih membatalkan transaksi saja — belum mengukuhkannya. Chapter berikutnya membahas START TRANSACTION beserta ragam cara membuka transaksi, termasuk mode SET autocommit = 0 dan jebakan implicit commit yang bisa membatalkan kehendak ROLLBACK kita.