DATABASE CONCURRENCY DATA-CONSISTENCY WEB-DEVELOPMENT BACKEND SYSTEM-DESIGN BEST-PRACTICES DATA-INTEGRITY SQL ORM TRANSACTION

Optimistic vs Pessimistic Locking: Mengelola Konkurensi Data untuk Aplikasi Web yang Konsisten

⏱️ 17 menit baca
👨‍💻

Optimistic vs Pessimistic Locking: Mengelola Konkurensi Data untuk Aplikasi Web yang Konsisten

1. Pendahuluan

Pernahkah kamu membayangkan apa yang terjadi jika dua pengguna, secara bersamaan, mencoba mengedit data yang sama di aplikasi webmu? Misalnya, dua pelanggan mencoba membeli stok terakhir suatu produk, atau dua administrator mencoba memperbarui status pesanan yang sama. Tanpa mekanisme yang tepat, ini bisa berujung pada kekacauan data: stok yang jadi minus, pesanan yang tidak jelas statusnya, atau bahkan uang yang hilang.

Fenomena ini dikenal sebagai konkurensi data, dan mengelolanya adalah salah satu tantangan krusial dalam membangun aplikasi web modern yang andal. Jika tidak ditangani dengan baik, konkurensi bisa menyebabkan race condition dan inkonsistensi data yang sulit dideteksi dan diperbaiki.

Di dunia pengembangan web, ada dua strategi utama untuk mengatasi masalah ini: Optimistic Locking dan Pessimistic Locking. Keduanya adalah pendekatan untuk memastikan bahwa data tetap konsisten meskipun diakses atau dimodifikasi oleh banyak pihak secara bersamaan.

Artikel ini akan membahas secara mendalam kedua strategi ini, menjelaskan cara kerjanya, kapan harus menggunakannya, serta kelebihan dan kekurangannya. Siapkan dirimu untuk memahami fondasi penting dalam menjaga integritas data aplikasi webmu!

2. Mengapa Konkurensi Data itu Tantangan?

Mari kita bayangkan sebuah skenario sederhana: kamu memiliki aplikasi e-commerce, dan ada sebuah produk dengan stok tersisa 1.

Skenario Masalah (Tanpa Pengelolaan Konkurensi):

  1. Pengguna A melihat produk tersebut, melihat stok 1, dan memutuskan untuk membelinya.
  2. Pada saat yang hampir bersamaan, Pengguna B juga melihat produk yang sama, melihat stok 1, dan juga memutuskan untuk membelinya.
  3. Aplikasi memproses permintaan Pengguna A:
    • Membaca stok: 1
    • Mengurangi stok: 1 - 1 = 0
    • Menyimpan stok baru: 0
  4. Pada saat yang sama, aplikasi memproses permintaan Pengguna B:
    • Membaca stok: 1 (karena Pengguna A belum selesai menyimpan perubahan)
    • Mengurangi stok: 1 - 1 = 0
    • Menyimpan stok baru: 0

📌 Hasilnya? Kedua pengguna berhasil membeli produk yang stoknya hanya 1. Stok di database menjadi 0, tapi dua transaksi penjualan terjadi. Ini adalah contoh klasik dari race condition yang menyebabkan lost update dan inkonsistensi data.

Secara kode, masalah ini mungkin terlihat seperti ini:

// Skenario di controller atau service
public function beliProduk($productId, $jumlah)
{
    $produk = Produk::find($productId); // SELECT * FROM produk WHERE id = :productId;

    if ($produk->stok >= $jumlah) {
        sleep(2); // Simulasikan proses bisnis yang butuh waktu

        $produk->stok -= $jumlah;
        $produk->save(); // UPDATE produk SET stok = :newStok WHERE id = :productId;

        return "Pembelian berhasil!";
    } else {
        return "Stok tidak cukup.";
    }
}

Jika fungsi beliProduk dipanggil dua kali secara bersamaan dengan jumlah = 1 dan stok = 1, sleep(2) akan memberikan celah bagi race condition untuk terjadi. Kedua proses akan membaca stok 1, lalu keduanya akan mencoba mengurangi dan menyimpan 0.

Untuk mencegah hal ini, kita memerlukan mekanisme “penguncian” atau “locking” yang memastikan hanya satu operasi yang bisa memodifikasi data pada satu waktu, atau setidaknya, memvalidasi bahwa data belum berubah sebelum disimpan.

3. Strategi 1: Pessimistic Locking – Kunci Sebelum Bertindak

Pessimistic Locking adalah pendekatan yang “pesimis” terhadap konkurensi. Ia berasumsi bahwa konflik akan terjadi, sehingga ia mengunci data secara eksplisit segera setelah data tersebut diakses untuk dimodifikasi. Mirip dengan konsep “siapa cepat dia dapat” dan “kunci dulu baru pakai”.

💡 Analogi: Bayangkan kamu ingin menggunakan satu-satunya toilet di sebuah kafe. Kamu tidak akan langsung masuk, tapi akan mengunci pintu dari dalam begitu kamu masuk. Selama kamu di dalam, tidak ada orang lain yang bisa masuk. Orang lain harus menunggu sampai kamu selesai dan membuka kuncinya.

Cara Kerja Pessimistic Locking

Dalam konteks database, Pessimistic Locking biasanya diimplementasikan menggunakan fitur penguncian transaksi (transactional locking) pada level baris (row-level lock) atau tabel (table-level lock).

  1. Ketika sebuah transaksi ingin membaca data dengan tujuan memodifikasinya, ia akan meminta lock pada baris data tersebut.
  2. Database akan memberikan lock, dan baris data tersebut tidak bisa dimodifikasi oleh transaksi lain sampai lock dilepaskan (biasanya ketika transaksi berakhir, baik commit atau rollback).
  3. Transaksi lain yang mencoba mengakses data yang terkunci akan menunggu (blocking) hingga lock dilepaskan, atau menerima error jika ada timeout.

Kapan Digunakan?

Kelebihan

Menjamin Integritas Data: Ini adalah metode paling aman untuk mencegah konflik data karena secara eksplisit mengunci resource. ✅ Mudah Dipahami: Konsepnya lugas: kunci, pakai, buka kunci.

Kekurangan

Menurunkan Performa: Transaksi lain harus menunggu (blocking), yang dapat memperlambat aplikasi dan mengurangi throughput. ❌ Potensi Deadlock: Jika dua transaksi mencoba mengunci dua resource yang sama secara berurutan yang berbeda, bisa terjadi deadlock (saling tunggu). ❌ Membutuhkan Koneksi Database Aktif: Lock dipegang selama koneksi database aktif, yang bisa boros resource jika lock dipegang terlalu lama.

Contoh Kode (PHP/Laravel dengan for update)

use Illuminate\Support\Facades\DB;
use App\Models\Produk;

public function beliProdukPessimistic($productId, $jumlah)
{
    // Memulai transaksi database
    DB::beginTransaction();
    try {
        // Mengambil produk dan menguncinya (row-level lock)
        // SELECT * FROM produk WHERE id = :productId FOR UPDATE;
        $produk = Produk::where('id', $productId)->lockForUpdate()->first();

        // ⚠️ Penting: Pastikan produk ditemukan dan stok cukup SETELAH dikunci
        if (!$produk) {
            DB::rollBack();
            return "Produk tidak ditemukan.";
        }

        if ($produk->stok >= $jumlah) {
            sleep(2); // Simulasikan proses bisnis yang butuh waktu

            $produk->stok -= $jumlah;
            $produk->save(); // UPDATE produk SET stok = :newStok WHERE id = :productId;

            DB::commit();
            return "Pembelian berhasil dengan Pessimistic Locking!";
        } else {
            DB::rollBack();
            return "Stok tidak cukup.";
        }
    } catch (\Exception $e) {
        DB::rollBack();
        return "Terjadi kesalahan: " . $e->getMessage();
    }
}

Dengan lockForUpdate(), jika ada dua permintaan yang masuk secara bersamaan, salah satu akan mendapatkan lock dan memproses, sementara yang lain akan menunggu hingga lock dilepaskan. Setelah yang pertama selesai, yang kedua akan melanjutkan, membaca stok yang sudah diperbarui (0), dan menemukan bahwa stok tidak cukup.

4. Strategi 2: Optimistic Locking – Periksa Sebelum Menyimpan

Optimistic Locking adalah pendekatan yang “optimis”. Ia berasumsi bahwa konflik jarang terjadi, sehingga tidak ada penguncian eksplisit. Data dibaca dan dimodifikasi tanpa mengunci. Namun, saat mencoba menyimpan perubahan, sistem akan memeriksa apakah data telah berubah oleh pihak lain sejak awal dibaca. Jika ya, konflik terdeteksi dan transaksi dibatalkan.

🎯 Analogi: Bayangkan kamu dan temanmu sama-sama mengunduh dan mengedit file dokumen yang sama di komputer masing-masing. Kalian berdua mengedit secara paralel. Ketika salah satu dari kalian mencoba mengunggah kembali file yang sudah diedit, sistem akan memeriksa apakah file di server sudah berubah. Jika sudah, sistem akan memberitahu bahwa ada konflik dan meminta kalian untuk mengatasinya (misalnya, menggabungkan perubahan secara manual atau mencoba lagi).

Cara Kerja Optimistic Locking

Optimistic Locking biasanya diimplementasikan dengan menambahkan kolom khusus pada tabel database, seperti version (integer) atau updated_at (timestamp).

  1. Ketika data dibaca, aplikasi juga membaca nilai kolom version atau updated_at tersebut.
  2. Aplikasi melakukan perubahan pada data di sisi memori tanpa mengunci database.
  3. Ketika aplikasi siap menyimpan perubahan, ia akan memperbarui data DAN mengincrement nilai version (atau memperbarui updated_at) dengan kondisi WHERE yang memastikan nilai version (atau updated_at) di database masih sama dengan yang dibaca pada langkah 1.
  4. Jika kondisi WHERE tidak terpenuhi (artinya version atau updated_at di database sudah berubah), maka tidak ada baris yang diperbarui. Ini menandakan adanya konflik. Aplikasi kemudian harus menangani konflik tersebut (misalnya, meminta user untuk mencoba lagi, atau menampilkan pesan error).

Kapan Digunakan?

Kelebihan

Performa Lebih Baik: Tidak ada blocking eksplisit, sehingga aplikasi dapat melayani lebih banyak permintaan secara paralel. ✅ Tidak Ada Deadlock: Karena tidak ada lock yang dipegang, potensi deadlock sangat minim. ✅ Skalabilitas: Lebih mudah diskalakan karena tidak ada resource yang terkunci secara eksklusif. ✅ Cocok untuk Web: Sesuai dengan sifat stateless HTTP.

Kekurangan

Membutuhkan Penanganan Konflik: Aplikasi harus memiliki logika untuk mendeteksi dan menangani konflik (misalnya, memberitahu pengguna untuk mencoba lagi, atau mengambil versi terbaru dan menggabungkan perubahan). ❌ Potensi Lost Update (Jika Salah Implementasi): Jika validasi version tidak dilakukan dengan benar, bisa saja terjadi lost update. ❌ Kompleksitas Implementasi: Menambahkan kolom version dan memastikan logika update yang benar di setiap tempat bisa jadi lebih kompleks.

Contoh Kode (PHP/Laravel dengan version column)

Asumsikan tabel produks memiliki kolom version (default 1).

use App\Models\Produk;

public function beliProdukOptimistic($productId, $jumlah)
{
    // 1. Ambil produk beserta versinya
    $produk = Produk::find($productId); // SELECT * FROM produk WHERE id = :productId;

    if (!$produk) {
        return "Produk tidak ditemukan.";
    }

    $initialStok = $produk->stok;
    $initialVersion = $produk->version;

    if ($initialStok >= $jumlah) {
        sleep(2); // Simulasikan proses bisnis yang butuh waktu

        // 2. Lakukan perubahan di memori
        $produk->stok -= $jumlah;
        $produk->version += 1; // Increment versi

        // 3. Coba simpan, periksa versi awal
        // UPDATE produk SET stok = :newStok, version = :newVersion
        // WHERE id = :productId AND version = :initialVersion;
        $affectedRows = DB::table('produks')
                        ->where('id', $productId)
                        ->where('version', $initialVersion)
                        ->update([
                            'stok' => $produk->stok,
                            'version' => $produk->version,
                            'updated_at' => now() // Penting untuk updated_at juga
                        ]);

        if ($affectedRows === 0) {
            // Jika 0 baris terpengaruh, berarti versi di database sudah berubah.
            // Konflik terdeteksi!
            return "Pembelian gagal: Data telah diubah oleh pengguna lain. Silakan coba lagi.";
        } else {
            return "Pembelian berhasil dengan Optimistic Locking!";
        }
    } else {
        return "Stok tidak cukup.";
    }
}

Dalam contoh ini, jika dua permintaan masuk, keduanya akan membaca stok = 1 dan version = 1. Permintaan pertama berhasil mengupdate stok = 0 dan version = 2. Ketika permintaan kedua mencoba mengupdate, kondisi WHERE id = :productId AND version = 1 tidak akan terpenuhi (karena version di DB sudah 2), sehingga affectedRows akan 0, menandakan konflik.

5. Membandingkan dan Memilih: Kapan Menggunakan yang Mana?

Memilih antara Optimistic dan Pessimistic Locking bukanlah masalah “mana yang lebih baik”, melainkan “mana yang lebih tepat” untuk skenario spesifik Anda.

Fitur / StrategiPessimistic LockingOptimistic Locking
PendekatanMengunci data secara eksplisit sebelum modifikasi.Tidak mengunci, memvalidasi versi saat menyimpan.
Asumsi KonflikKonflik akan terjadi.Konflik jarang terjadi.
Kapan Terjadi LockSaat membaca data yang akan dimodifikasi.Tidak ada lock eksplisit.
PerformaLebih rendah (potensi blocking).Lebih tinggi (tidak ada blocking).
Integritas DataSangat terjamin.Terjamin jika penanganan konflik di aplikasi tepat.
Potensi DeadlockYa.Tidak ada.
Kompleksitas Impl.Lebih sederhana di aplikasi, tapi perlu kelola transaksi.Menambah kolom versi, perlu logika penanganan konflik.
User ExperiencePengguna menunggu atau error.Pengguna mungkin perlu mencoba ulang.
SkalabilitasLebih rendah (karena blocking).Lebih tinggi (tanpa blocking).

Kapan Menggunakan Pessimistic Locking?

Kapan Menggunakan Optimistic Locking?

6. Tips Praktis dan Best Practices

Untuk Pessimistic Locking:

Untuk Optimistic Locking:

Umum:

Kesimpulan

Mengelola konkurensi data adalah aspek fundamental dalam membangun aplikasi web yang andal dan konsisten. Optimistic Locking dan Pessimistic Locking adalah dua strategi ampuh yang bisa kamu gunakan, masing-masing dengan kelebihan dan kekurangannya sendiri.

Pessimistic Locking cocok untuk skenario di mana integritas data mutlak adalah prioritas utama dan konflik diperkirakan tinggi, meskipun harus mengorbankan sedikit performa. Ini seperti “mengunci pintu” untuk memastikan tidak ada yang mengganggu.

Sebaliknya, Optimistic Locking lebih cocok untuk skenario di mana performa dan skalabilitas lebih diutamakan, konflik jarang terjadi,