DISTRIBUTED-SYSTEMS TRANSACTIONS DATA-CONSISTENCY MICROSERVICES SOFTWARE-ARCHITECTURE DATABASE SYSTEM-DESIGN ATOMICITY BACKEND

Transaksi Terdistribusi: Memahami Two-Phase Commit (2PC) dan Tantangannya

⏱️ 14 menit baca
👨‍💻

Transaksi Terdistribusi: Memahami Two-Phase Commit (2PC) dan Tantangannya

1. Pendahuluan

Di dunia aplikasi modern, terutama dengan adopsi microservices dan sistem terdistribusi yang masif, kita sering berhadapan dengan data yang tersebar di banyak layanan atau database yang terpisah. Bayangkan skenario sederhana: Anda memesan barang online. Proses ini melibatkan pengurangan stok di layanan inventori, pemrosesan pembayaran di layanan pembayaran, dan pembuatan entri pesanan di layanan order. Apa yang terjadi jika salah satu dari langkah-langkah ini gagal? Tentu kita tidak ingin stok berkurang tapi pembayaran gagal, atau pembayaran sukses tapi pesanan tidak tercatat.

Inilah inti dari tantangan transaksi terdistribusi. Di database monolitik, kita punya properti ACID (Atomicity, Consistency, Isolation, Durability) yang menjamin bahwa serangkaian operasi akan berjalan semua atau tidak sama sekali. Namun, mencapai Atomicity di sistem terdistribusi, di mana banyak layanan atau database independen perlu berkoordinasi, adalah masalah yang jauh lebih kompleks.

Salah satu protokol klasik yang dirancang untuk mengatasi masalah ini adalah Two-Phase Commit (2PC). Meskipun hari ini sering dianggap sebagai anti-pattern di banyak arsitektur microservices modern, memahami 2PC adalah kunci untuk mengerti dasar-dasar konsistensi di sistem terdistribusi dan alasan mengapa pola-pola alternatif seperti Saga Pattern muncul.

Mari kita selami lebih dalam! 🚀

2. Apa Itu Transaksi Terdistribusi?

Transaksi terdistribusi adalah serangkaian operasi yang melibatkan beberapa sumber daya atau layanan yang terpisah, namun harus diperlakukan sebagai satu unit atomik. Artinya, semua operasi dalam transaksi tersebut harus berhasil di-commit, atau semuanya harus di-rollback. Tidak ada keadaan di mana hanya sebagian operasi yang berhasil.

Contoh Klasik: Transfer uang antar dua bank yang berbeda.

  1. Kurangi saldo di Bank A.
  2. Tambah saldo di Bank B.

Jika Bank A berhasil mengurangi saldo tapi Bank B gagal menambahkannya (misalnya karena sistem Bank B down), maka uang tersebut akan hilang di tengah jalan. Ini adalah bencana! Transaksi terdistribusi bertujuan untuk mencegah skenario seperti ini.

Dalam konteks microservices, contohnya bisa lebih kompleks:

Tujuan utama adalah menjaga konsistensi data di seluruh sistem, bahkan ketika data tersebar di berbagai layanan atau database yang berbeda.

3. Memahami Two-Phase Commit (2PC)

Two-Phase Commit (2PC) adalah protokol konsensus terdistribusi yang memastikan semua peserta dalam sebuah transaksi terdistribusi setuju untuk melakukan commit atau rollback. Protokol ini melibatkan dua jenis entitas utama:

  1. Coordinator (atau Transaction Manager): Entitas yang bertanggung jawab mengelola alur transaksi, berkomunikasi dengan peserta, dan membuat keputusan commit/rollback.
  2. Participants (atau Resource Managers): Layanan atau database yang terlibat dalam transaksi dan memiliki sumber daya yang perlu diubah.

Seperti namanya, 2PC berjalan dalam dua fase utama: Prepare Phase dan Commit Phase.

3.1. Fase 1: Prepare (Voting Phase)

Pada fase ini, Coordinator meminta semua Participants untuk “mempersiapkan” diri untuk melakukan commit.

📌 Alur Kerja Prepare Phase:

  1. Coordinator mengirim pesan prepare ke semua Participants yang terlibat dalam transaksi. Pesan ini berisi detail transaksi yang akan dilakukan.
  2. Setiap Participant menerima pesan prepare:
    • Participant memeriksa apakah mereka dapat menyelesaikan transaksi. Ini mungkin melibatkan pengecekan ketersediaan sumber daya, validasi data, atau mengunci sumber daya yang relevan.
    • Jika Participant siap dan mampu untuk commit, mereka menulis semua perubahan ke log transaksi lokal mereka (dikenal sebagai undo/redo logs) dan mengunci sumber daya yang terlibat. Mereka kemudian mengirim respons yes (atau vote-commit) ke Coordinator.
    • Jika Participant tidak dapat commit (misalnya, karena sumber daya tidak cukup, error, atau sistem down), mereka melakukan rollback perubahan lokal dan mengirim respons no (atau vote-abort) ke Coordinator.

💡 Poin Penting: Setelah Participant mengirim yes, ia harus menjaga sumber daya tetap terkunci dan siap untuk commit atau rollback. Ia tidak dapat mengambil keputusan sendiri sampai menerima instruksi dari Coordinator.

// Pseudo-code: Coordinator side (Prepare Phase)
public enum Vote { YES, NO }

public class Coordinator {
    private List<Participant> participants; // List of services/DBs
    private Transaction transaction;

    public boolean prepare() {
        boolean allVotedYes = true;
        for (Participant p : participants) {
            try {
                Vote vote = p.requestPrepare(transaction);
                if (vote == Vote.NO) {
                    allVotedYes = false;
                    break; // One 'NO' means global abort
                }
            } catch (Exception e) {
                // Participant failed to respond or threw error
                allVotedYes = false;
                break;
            }
        }
        return allVotedYes;
    }
    // ... next phase
}

// Pseudo-code: Participant side (Prepare Phase)
public class Participant {
    public Vote requestPrepare(Transaction transaction) {
        // 1. Check if resource is available and can commit
        // 2. If yes, write changes to local log (undo/redo)
        // 3. Lock relevant resources
        // 4. Return YES
        // 5. If no, rollback local changes (if any) and return NO
        if (canCommit(transaction)) {
            logChanges(transaction);
            lockResources(transaction);
            return Vote.YES;
        } else {
            rollbackLocalChanges(transaction); // If any temporary changes were made
            return Vote.NO;
        }
    }
    // ... next phase
}

3.2. Fase 2: Commit/Rollback (Decision Phase)

Setelah Coordinator mengumpulkan semua respons dari Participants, ia membuat keputusan akhir: apakah akan melakukan commit atau rollback seluruh transaksi.

🎯 Alur Kerja Commit/Rollback Phase:

  1. Coordinator mengevaluasi hasil voting:
    • Jika semua Participants mengirim yes, Coordinator memutuskan untuk melakukan commit.
    • Jika setidaknya satu Participant mengirim no, atau jika ada Participant yang gagal merespons dalam batas waktu tertentu, Coordinator memutuskan untuk melakukan rollback.
  2. Coordinator mengirim pesan keputusan (commit atau rollback) ke semua Participants.
  3. Setiap Participant menerima pesan keputusan:
    • Jika menerima commit, Participant secara permanen menerapkan perubahan yang telah ditulis ke log lokal dan melepaskan kunci sumber daya. Kemudian, mereka mengirim respons ack (acknowledgement) ke Coordinator.
    • Jika menerima rollback, Participant membatalkan semua perubahan yang telah disiapkan (menggunakan log undo) dan melepaskan kunci sumber daya. Kemudian, mereka mengirim respons ack ke Coordinator.

Sukses: Semua Participants commit. ❌ Gagal: Semua Participants rollback.

// Pseudo-code: Coordinator side (Commit/Rollback Phase)
public class Coordinator {
    // ... previous prepare method

    public void commitOrRollback(boolean allVotedYes) {
        if (allVotedYes) {
            // Send commit to all participants
            for (Participant p : participants) {
                p.requestCommit(transaction);
            }
            System.out.println("Global Commit: All participants committed.");
        } else {
            // Send rollback to all participants
            for (Participant p : participants) {
                p.requestRollback(transaction);
            }
            System.out.println("Global Rollback: At least one participant aborted.");
        }
    }
}

// Pseudo-code: Participant side (Commit/Rollback Phase)
public class Participant {
    // ... previous requestPrepare method

    public void requestCommit(Transaction transaction) {
        // 1. Permanently apply changes from local log
        // 2. Release locked resources
        // 3. Send ACK to Coordinator
        applyChanges(transaction);
        releaseResources(transaction);
        sendAckToCoordinator();
    }

    public void requestRollback(Transaction transaction) {
        // 1. Undo changes using local log
        // 2. Release locked resources
        // 3. Send ACK to Coordinator
        undoChanges(transaction);
        releaseResources(transaction);
        sendAckToCoordinator();
    }
}

4. Ilustrasi 2PC dengan Contoh Nyata: Sistem Pemesanan Online

Mari kita gunakan contoh sistem pemesanan online dengan tiga layanan:

Coordinator: Aplikasi backend utama yang menerima permintaan pesanan. Participants: Order Service, Payment Service, Inventory Service.

Skenario Sukses:

  1. Coordinator menerima permintaan createOrder(userId, productId, quantity).
  2. Fase 1 (Prepare):
    • Coordinator mengirim prepare ke Order Service, Payment Service, Inventory Service.
    • Order Service: Membuat entri pesanan sementara, mengunci ID pesanan, merespons yes.
    • Payment Service: Memverifikasi metode pembayaran, membuat transaksi pembayaran sementara, merespons yes.
    • Inventory Service: Memeriksa ketersediaan stok, mengurangi stok sementara, mengunci stok, merespons yes.
  3. Coordinator menerima yes dari semua Participants.
  4. Fase 2 (Commit):
    • Coordinator mengirim commit ke Order Service, Payment Service, Inventory Service.
    • Order Service: Menyimpan pesanan secara permanen, melepaskan kunci, merespons ack.
    • Payment Service: Menyelesaikan transaksi pembayaran, melepaskan kunci, merespons ack.
    • Inventory Service: Mengurangi stok secara permanen, melepaskan kunci, merespons ack.
  5. Coordinator menerima ack dari semua Participants. Transaksi createOrder berhasil sepenuhnya.

Skenario Gagal (Misal: Stok Tidak Cukup):

  1. Coordinator menerima permintaan createOrder.
  2. Fase 1 (Prepare):
    • Coordinator mengirim prepare ke semua layanan.
    • Order Service: yes.
    • Payment Service: yes.
    • Inventory Service: Memeriksa stok, ternyata stok tidak cukup. Merespons no.
  3. Coordinator menerima no dari Inventory Service.
  4. Fase 2 (Rollback):
    • Coordinator mengirim rollback ke Order Service, Payment Service, Inventory Service.
    • Order Service: Menghapus entri pesanan sementara, melepaskan kunci, merespons ack.
    • Payment Service: Membatalkan transaksi pembayaran sementara, melepaskan kunci, merespons ack.
    • Inventory Service: Mengembalikan stok ke kondisi awal (jika ada perubahan sementara), melepaskan kunci, merespons ack.
  5. Coordinator menerima ack dari semua Participants. Transaksi createOrder gagal sepenuhnya, tidak ada perubahan yang tersisa.

5. Tantangan dan Kekurangan Two-Phase Commit

Meskipun 2PC menjamin atomicity, protokol ini memiliki beberapa kelemahan signifikan yang membuatnya kurang cocok untuk sebagian besar arsitektur sistem terdistribusi modern, terutama microservices.

⚠️ Kekurangan Utama 2PC:

  1. Blocking (Blokir Sumber Daya):

    • Selama Fase Prepare, Participants mengunci sumber daya yang terlibat. Jika Coordinator gagal (crash) setelah mengirim pesan prepare tetapi sebelum mengirim commit atau rollback, Participants akan tetap dalam keadaan “siap commit” dan sumber daya mereka akan tetap terkunci tanpa batas waktu. Ini menyebabkan deadlock dan sistem tidak dapat melanjutkan.
    • Pemulihan dari kondisi blocking ini sangat kompleks dan seringkali memerlukan intervensi manual.
  2. Single Point of Failure (SPOF):

    • Coordinator adalah titik pusat dalam protokol ini. Jika Coordinator gagal di tengah proses, seluruh transaksi akan terhenti dan dapat menyebabkan kondisi blocking seperti yang dijelaskan di atas.
    • Meskipun ada upaya untuk membuat Coordinator lebih fault-tolerant (misalnya dengan replikasi), ini menambah kompleksitas yang signifikan.
  3. Performance Overhead:

    • 2PC memerlukan banyak komunikasi jaringan antar Coordinator dan Participants (setidaknya 4 pesan per Participants untuk sukses: prepare, yes, commit, ack).
    • Locking sumber daya yang lama juga dapat mengurangi konkurensi dan performa sistem secara keseluruhan.
    • Latensi jaringan yang tinggi antar layanan akan memperlambat seluruh transaksi.
  4. Kompleksitas Implementasi dan Debugging:

    • Membangun sistem 2PC yang robust, terutama dalam hal pemulihan dari kegagalan, sangat sulit. Debugging masalah konsistensi di lingkungan terdistribusi yang melibatkan banyak pihak juga merupakan tantangan besar.
  5. Melanggar Prinsip Otonomi Microservices:

    • Filosofi microservices menekankan layanan yang independen dan otonom. 2PC mengharuskan layanan untuk berkoordinasi secara ketat dan menunggu keputusan dari Coordinator eksternal, yang mengurangi otonomi layanan dan meningkatkan coupling.

Karena tantangan-tantangan ini, 2PC jarang digunakan dalam implementasi langsung di tingkat aplikasi untuk microservices. Namun, konsep dasarnya masih relevan dan sering diterapkan dalam implementasi internal database terdistribusi atau sistem messaging tertentu.

6. Alternatif dan Solusi Modern

Mengingat kelemahan 2PC, komunitas developer telah beralih ke pola-pola lain yang lebih sesuai untuk arsitektur terdistribusi modern, yang seringkali mengorbankan konsistensi atomik instan demi ketersediaan dan performa (sesuai Teorema CAP).

Kesimpulan

Two-Phase Commit (2PC) adalah protokol fundamental yang mengajarkan kita tentang tantangan dalam mencapai atomicity di sistem terdistribusi. Meskipun secara teori menjanjikan konsistensi yang kuat, kelemahan inherennya seperti blocking, SPOF, dan overhead performa membuatnya kurang praktis untuk sebagian besar arsitektur microservices modern.

Memahami 2PC membantu kita mengapresiasi mengapa pola-pola seperti Saga Pattern dan Transactional Outbox Pattern menjadi pilihan yang lebih populer. Pola-pola ini seringkali memilih eventual consistency dan kompensasi sebagai strategi untuk mengelola transaksi terdistribusi, demi mencapai skalabilitas, ketersediaan, dan otonomi layanan yang lebih baik.

Sebagai developer, penting untuk memahami trade-off antara konsistensi, ketersediaan, dan partisi (sesuai Teorema CAP) saat merancang sistem terdistribusi. Pilihlah pola dan strategi yang paling sesuai dengan kebutuhan bisnis dan toleransi risiko inkonsistensi data Anda.

🔗 Baca Juga