Pessimistic vs Optimistic Locking: Kapan harus memilih?

Race condition

Saat kita mengembangkan sistem yang melibatkan akses bersama ke data, salah satu tantangan utama yang kita hadapi adalah mengelola konsistensi data. Misalnya, bayangkan Anda sedang membangun sebuah aplikasi sistem pemesanan tiket. Banyak orang mungkin mencoba untuk memesan tiket untuk acara yang sama pada waktu yang hampir bersamaan. Jika tidak hati-hati, bisa terjadi race condition atau bahkan inconsistency data.

Untuk mengatasi masalah ini, kita membutuhkan mekanisme yang dapat mencegah konflik akses data. Salah satu cara yang umum digunakan adalah locking, di mana kita "mengunci" data sementara seseorang mengubahnya. Dua pendekatan yang paling sering digunakan adalah pessimistic locking dan optimistic locking.

Pada artikel ini, saya akan membahas kedua teknik tersebut, memberikan contoh penerapannya, serta membantu Anda memilih yang paling sesuai untuk kebutuhan sistem Anda.

πŸ”’ Pessimistic Locking: Menggunakan SELECT FOR UPDATE di SQL

Apa Itu Pessimistic Locking?

Pessimistic Locking mengunci baris saat dibaca, mencegah transaksi lain untuk mengakses baris tersebut selama transaksi yang sedang berlangsung. Ini mirip dengan mengatakan, β€œSaya akan mengunci data ini agar tidak ada yang mengubahnya sampai saya selesai.”

βœ… Kelebihan:

  • Konsistensi Data Terjamin: Tidak ada risiko perubahan data yang tidak sah.
  • Cocok untuk Sistem dengan Transaksi yang Kritis: Seperti aplikasi pembayaran atau sistem stok barang yang sering diubah oleh banyak pengguna.

❌ Kekurangan:

  • Menurunkan Performa pada Skala Besar: Pengguna lain harus menunggu sampai kunci dibuka.
  • Potensi Deadlock: Jika dua transaksi saling menunggu untuk mengakses data yang sama, sistem bisa macet.
Pessimistic Locking

Contoh Kasus:

Misalkan kita memiliki sistem yang menangani saldo akun pengguna dan ingin mentransfer sejumlah uang dari satu akun ke akun lainnya. Kita akan menggunakan pessimistic locking untuk memastikan tidak ada dua transaksi yang mengubah saldo akun yang sama pada waktu yang bersamaan.

Langkah-langkah Implementasi:

  • Mengambil saldo akun dan mengunci baris untuk mencegah akses bersamaan.
  • Melakukan transfer uang.
  • Memastikan bahwa saldo tidak berubah selama proses transaksi.

Contoh Kode

package main

import (
	"database/sql"
	"errors"
	"fmt"
	"log"

	_ "github.com/lib/pq"
)

func transferFunds(db *sql.DB, fromAccountID, toAccountID int, amount float64) error {
	// Mulai transaksi
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// Mengunci baris untuk akun yang akan diubah
	// Ini akan mencegah transaksi lain mengakses akun yang sama sampai transaksi ini selesai
	var fromBalance, toBalance float64
	err = tx.QueryRow(`SELECT balance FROM accounts WHERE id = $1 FOR UPDATE`, fromAccountID).Scan(&fromBalance)
	if err != nil {
		return err
	}
	err = tx.QueryRow(`SELECT balance FROM accounts WHERE id = $1 FOR UPDATE`, toAccountID).Scan(&toBalance)
	if err != nil {
		return err
	}

	// Cek apakah saldo cukup
	if fromBalance < amount {
		return errors.New("insufficient funds")
	}

	// Melakukan transfer
	_, err = tx.Exec(`UPDATE accounts SET balance = balance - $1 WHERE id = $2`, amount, fromAccountID)
	if err != nil {
		return err
	}
	_, err = tx.Exec(`UPDATE accounts SET balance = balance + $1 WHERE id = $2`, amount, toAccountID)
	if err != nil {
		return err
	}

	// Commit transaksi
	if err := tx.Commit(); err != nil {
		return err
	}

	return nil
}

func main() {
	// Koneksi ke database (misalnya PostgreSQL)
	connStr := "user=username dbname=testdb sslmode=disable"
	db, err := sql.Open("postgres", connStr)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Contoh panggilan fungsi transfer
	err = transferFunds(db, 1, 2, 100)
	if err != nil {
		log.Fatal(err)
	} else {
		fmt.Println("Transfer berhasil!")
	}
}

Penjelasan:

  • Kita menggunakan SQL SELECT FOR UPDATE untuk mengunci baris yang sedang diambil, sehingga tidak ada transaksi lain yang bisa mengubah data tersebut sampai transaksi ini selesai.
  • Transaksi hanya akan di-commit jika semuanya berjalan lancar.
  • Jika terjadi kekurangan saldo, maka transaksi akan dibatalkan dan perubahan tidak akan disimpan.

🀝 Optimistic Locking: Menggunakan Version Field untuk Validasi

Optimistic Locking tidak mengunci data saat dibaca. Sebaliknya, ia melacak versi data, dan sebelum menyimpan perubahan, ia memeriksa apakah data telah diubah oleh transaksi lain.

βœ… Kelebihan:

  • Performa Lebih Baik: Tidak ada penguncian yang memblokir pengguna lain.
  • Cocok untuk Aplikasi dengan Banyak Pembacaan dan Sedikit Penulisan: Seperti aplikasi laporan atau sosial media.

❌ Kekurangan:

  • Menambah Kompleksitas: Anda perlu menangani konflik jika data sudah berubah.
  • Rentan Terhadap Pengguna yang Sering Mengubah Data yang Sama.
Optimistic Locking

Contoh Kasus:

Misalkan kita memiliki aplikasi manajemen tugas di mana pengguna dapat mengedit tugas. Setiap tugas memiliki versi (version). Jika dua orang mencoba mengedit tugas yang sama pada waktu yang bersamaan, sistem akan mendeteksi konflik dan meminta pengguna untuk memperbarui perubahan mereka.

Langkah-langkah Implementasi:

  • Membaca data dengan field version.
  • Melakukan perubahan pada tugas.
  • Sebelum commit, pastikan tidak ada yang mengubah tugas yang sama.

Contoh Kode


package main

import (
	"database/sql"
	"errors"
	"fmt"
	"log"

	_ "github.com/lib/pq"
)

type Task struct {
	ID      int
	Name    string
	Version int
}

func updateTask(db *sql.DB, task Task) error {
	// Melakukan update dengan memeriksa versi
	// Jika versi berbeda, berarti ada perubahan dari transaksi lain
	res, err := db.Exec(`
		UPDATE tasks 
		SET name = $1, version = version + 1 
		WHERE id = $2 AND version = $3`,
		task.Name, task.ID, task.Version,
	)

	if err != nil {
		return err
	}

	rowsAffected, _ := res.RowsAffected()
	if rowsAffected == 0 {
		return errors.New("conflict detected: task has been updated by another user")
	}

	return nil
}

func main() {
	// Koneksi ke database (misalnya PostgreSQL)
	connStr := "user=username dbname=testdb sslmode=disable"
	db, err := sql.Open("postgres", connStr)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// Contoh task yang akan diupdate
	task := Task{
		ID:      1,
		Name:    "Update Project",
		Version: 1, // Versi yang saat ini ada di database
	}

	// Melakukan update
	err = updateTask(db, task)
	if err != nil {
		log.Fatal(err)
	} else {
		fmt.Println("Task berhasil diupdate!")
	}
}

Penjelasan:

  • Pada contoh ini, kita menggunakan field version untuk memeriksa apakah data telah diubah oleh transaksi lain sebelum melakukan update.
  • Jika versi yang diterima tidak cocok dengan versi yang ada di database, berarti data telah diubah oleh pengguna lain, dan transaksi akan gagal.
  • Ini menghindari konflik data dan memastikan bahwa hanya perubahan yang valid yang diterapkan.

πŸ“Š Perbandingan Singkat

KriteriaPessimistic LockingOptimistic Locking
KinerjaLebih rendah pada skala besarLebih tinggi karena tanpa kunci
Risiko KonflikHampir tidak adaMungkin terjadi, dicegah via versi
Cocok UntukTransaksi finansial, inventarisAplikasi kolaboratif, task editor

🧠 Kesimpulan

  • Pessimistic Locking lebih cocok untuk sistem yang membutuhkan konsistensi data yang tinggi, seperti aplikasi transaksi finansial, sistem pembelian tiket, atau aplikasi dengan tingkat konflik data yang tinggi.
  • Optimistic Locking lebih cocok untuk aplikasi dengan frekuensi pembacaan data lebih tinggi dan lebih sedikit pembaruan, seperti aplikasi manajemen tugas, aplikasi e-commerce dengan produk yang banyak dibaca tapi jarang dibeli.

Pilih teknik yang sesuai dengan karakteristik aplikasi dan kebutuhan performa yang Anda hadapi. Semoga contoh dan penjelasan ini membantu Anda dalam memilih dan mengimplementasikan teknik yang tepat dalam sistem yang sedang dibangun.

Referensi