author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Pessimistic Locking vs Optimistic Locking: Mencegah Update Anomaly
Tuesday Jan 31st, 2023 12:35 pm8 mins read
Tips & Tutorial, Programming Principle, My Views
Pessimistic Locking vs Optimistic Locking: Mencegah Update Anomaly
Source: Freepik - Locking up or unlocking door with key in hand

Jika pada tulisan Isolation pada ACID kita membahas tentang Read Phenomena, maka kali ini pembahasannya tentang Update Anomaly. Read Phenomena adalah fenomena selisih pembacaan data pada sebagian data pada suatu sesi karena ada perubahan oleh sesi lain secara bersamaan. Sedangkan Update Anomaly adalah fenomena selisih hasil perubahan data yang dilakukan pada suatu sesi karena sesi lain juga melakukan commit perubahan pada data yang sama secara bersamaan sehingga hasil perubahannya tumpang-tindih. Secara umum Update Anomaly terbagi 2, yaitu Write Skew dan Lost Updates. Kita akan memakai contoh data berikut:

Table fruit

id name stock expiring_date
1 Apel 20 2023-01-31
2 Pisang 1 2023-01-30

Write Skew

Write Skew artinya terjadi perubahan data yang sama pada kolom yang berbeda secara serentak oleh lebih dari satu sesi sehingga mendapatkan hasil perubahan yang tidak sesuai dengan command update yang dieksekusi. Contohnya seperti berikut:

Sesi A

BEGIN;

SELECT *
FROM fruit
WHERE id = 1;

Sesi A dimulai dengan melakukan selection pada fruit dengan id = 1 yaitu Apel dengan stok 20 buah.

id name stock expiring_date
1 Apel 20 2023-01-31

Sesi B

BEGIN;

UPDATE fruit
SET name = 'Semangka'
WHERE id = 1;

COMMIT;

Di saat bersamaan, sesi B mengganti nama buah menjadi Semangka lalu di-commit.

id name stock expiring_date
1 Semangka 20 2023-01-31

Sesi A

UPDATE fruit
SET stock = stock - 1
WHERE id = 1;

SELECT *
FROM fruit
WHERE id = 1;

Sesi A kemudian melakukan perubahan pada id = 1 dengan mengurangi stok 1 buah. Lalu Sesi A melakukan query kembali untuk mengecek perubahannya. Ekspektasi sesi A adalah buah Apel dengan id = 1 stoknya berkurang menjadi 19. Tapi yang terjadi adalah, stoknya berkurang menjadi 19 dan nama buahnya berubah menjadi Semangka.

id name stock expiring_date
1 Semangka 19 2023-01-31

Kondisi tersebut tentu membuat user pada Sesi A bingung karena buahnya berubah menjadi Semangka, padahal dia hanya mengubah stock.

Lost Updates

Lost Updates adalah kondisi ketika perubahan pada data dan kolom yang sama yang dilakukan lebih dari satu sesi secara bersamaan sehingga perubahan tersebut tidak diketahui oleh sesi lain. Contohnya seperti ini:

Sesi A

BEGIN;

SELECT *
FROM fruit
WHERE id = 2;

Sesi A melakukan query selection pada buah dengan id = 2 yaitu Pisang dengan stok 1 buah.

id name stock expiring_date
2 Pisang 1 2023-01-30

Sesi B

BEGIN;

UPDATE fruit
SET stock = stock - 1
WHERE id = 2;

COMMIT;

Secara bersamaan Sesi B mengurangi stok pada buah dengan id = 2 sebanyak 1 buah dan di-commit sehingga stoknya sekarang jadi 0.

id name stock expiring_date
2 Pisang 0 2023-01-30

Sesi A

UPDATE fruit
SET stock = stock - 1
WHERE id = 2;

SELECT *
FROM fruit
WHERE id = 2;

Sesi A juga mengurangi stok pada buah tersebut sebanyak 1 buah. Lalu mengecek kembali hasil perubahannya. Ekspektasinya adalah stok Pisang pada Sesi A berkurang menjadi 0 karena sebelumnya stoknya berjumlah 1. Tapi ternyata stoknya malah -1.

id name stock expiring_date
2 Pisang -1 2023-01-30

Sesi A kehilangan jejak akibat perubahan yang dilakukan oleh Sesi B. Kondisi tersebut sebenarnya sah-sah saja dan bukan Dirty Read karena perubahan pada Sesi B sudah di-commit. Tapi stoknya berubah jadi minus dan membuat aplikasi menjadi buggy karena secara bisnis ga mungkin stok bernilai minus. Anomaly di atas dapat dicegah menggunakan Pessimistic Locking maupun Optimistic Locking.

Pessimistic Locking

Pessimistic Locking adalah proses penguncian data terhadap perubahan dari sesi lain yang dilakukan dari sisi Database.

Serializable Isolation

Seperti yang sudah gw jelaskan sebelumnya, Serializable Isolation terhindar dari Phenomena apapun, termasuk Update Anomaly. Serializable Isolation memblok transaksi data yang sama yang dilakukan oleh sesi lain sehingga tidak terjadi tumpang tindih perubahan data. Ini dapat mencegah Write Skew maupun Lost Updates. Kekurangannya adalah performa database akan jadi lebih berat. Untuk lebih jelasnya bisa dicek kembali tulisan gw tentang Serializable Isolation karena gw males nulis ulang disini😝.

Select For Update

Ini adalah salah satu fitur dari Database yang dapat mengunci data yang di-select agar data tersebut tidak bisa diubah oleh sesi lain sampai transaksi tersebut selesai. Caranya adalah dengan menambahkan suffix FOR UPDATE pada akhir query selection seperti ini:

Sesi A

BEGIN;

SELECT *
FROM fruit
WHERE id = 2 FOR UPDATE;

Sesi A melakukan selection pada buah dengan id = 2 yaitu Pisang dengan stok 1 buah tapi dengan query Select For Update.

id name stock expiring_date
2 Pisang 1 2023-01-30

Sesi B

BEGIN;

UPDATE fruit
SET stock = stock - 1,
    name = 'Alpukat'
WHERE id = 2;

COMMIT;

Di saat bersamaan Sesi B melakukan perubahan dengan mengganti nama buah pada id = 2 menjadi Alpukat dan stoknya berkurang 1. Di sini perubahan pada Sesi B akan diblok karena harus menunggu Sesi A menyelesaikan transaksinya.

Sesi A

UPDATE fruit
SET stock = stock - 1
WHERE id = 2;

SELECT *
FROM fruit
WHERE id = 2;

Sesi A kemudian mengurangi stok Pisang sebanyak 1 buah dan mengecek hasil perubahannya. Hasilnya adalah muncul data Pisang dengan stok yang berkurang 1 menjadi 0 sesuai ekspektasi.

id name stock expiring_date
2 Pisang 0 2023-01-30

Sekarang Sesi A sudah terhindar dari Write Skew & Lost Updates😎. Ketika perubahan pada Sesi A di-commit, Sesi B dapat melanjutkan perubahannya. Masalahnya adalah Sesi B juga akan mengurangi stok buah dengan id = 2 tersebut sebanyak 1 buah sehingga stoknya akan menjadi -1. Lost Updates sekarang justru terjadi pada Sesi B🤭.

Optimistic Locking

Optimistic Locking adalah proses penguncian data terhadap perubahan dari sesi lain yang dilakukan dari sisi algoritma aplikasi. Jadi sebenarnya ga ada “penguncian” secara fisik disini, hanya menambahkan sedikit logika update secara konsisten. Secara umum Optimistic Locking ada 2, yaitu All Column dan Version Column.

All Column

Caranya adalah dengan menambahkan semua kolom pada where clause dengan value sebelumnya saat melakukan update. Biar ga bingung bisa liat contoh berikut:

Sesi A

BEGIN;

SELECT *
FROM fruit
WHERE id = 2;

Sesi A melakukan selection pada buah dengan id = 2 yaitu Pisang dengan stok 1 buah.

id name stock expiring_date
2 Pisang 1 2023-01-30

Sesi B

BEGIN;

SELECT *
FROM fruit
WHERE id = 2;

UPDATE fruit
SET stock = stock - 1,
    name = 'Alpukat'
WHERE id = 2 AND 
      stock = 1 AND 
      name ='Pisang' AND
      expiring_date = '2023-01-30';

COMMIT;

Di saat bersamaan Sesi B melakukan perubahan dengan mengganti nama buah pada id = 2 menjadi Alpukat dan stoknya dikurangi 1 lalu di-commit.

id name stock expiring_date
2 Alpukat 0 2023-01-30

Sesi A

UPDATE fruit
SET stock = stock - 1
WHERE id = 2 AND 
      stock = 1 AND 
      name ='Pisang' AND
      expiring_date = '2023-01-30';

Sesi A lalu mengurangi stok Pisang sebanyak 1 buah dengan menambahkan where clause semua kolom dengan value yang di-select sebelumnya, yaitu id, name, stock, dan expiring_date. Karena pada Sesi B stoknya sudah berubah jadi 0 dan namanya berubah jadi Alpukat, maka proses update pada Sesi A tidak dapat dilakukan karena datanya sudah tidak ditemukan. Sekarang data tersebut tidak dapat lagi diubah secara serentak dan terhindar dari Write Skew & Lost Updates😎.

Version Column

Ini adalah cara paling umum diterapkan karena hanya butuh minimal dua kolom where clause saat update, yaitu id dan version. Kita harus menambahkan kolom baru yaitu version pada table. Tabelnya jadi seperti berikut:

id name stock expiring_date version
1 Apel 20 2023-01-31 1
2 Pisang 1 2023-01-30 1

Algoritmanya adalah ketika melakukan insertion, kita akan mengisi kolom version dengan angka 0. Lalu setiap terjadi perubahan, kolom version itu akan ikut di-update secara incremental. Misalnya versionnya sekarang 1, lalu setelah dilakukan update sekali, versionnya ikut di-update jadi 2, begitu seterusnya setiap terjadi update. Saat melakukan update, kita tidak perlu memasukkan semua kolom sebagai where clause seperti All Column, melainkan hanya memasukkan id dan value version yang lama. Command updatenya jadi lebih simple dibanding All Column. Contohnya seperti berikut:

Sesi A

BEGIN;

SELECT *
FROM fruit
WHERE id = 2;

Sesi A melakukan query selection pada buah dengan id = 2.

id name stock expiring_date version
2 Pisang 1 2023-01-30 1

Sesi B

BEGIN;

SELECT *
FROM fruit
WHERE id = 2;

UPDATE fruit
SET stock = stock - 1,
    name = 'Alpukat',
    version = version + 1
WHERE id = 2 AND 
      version = 1;

COMMIT;

Di saat bersamaan Sesi B melakukan perubahan dengan mengganti nama buah pada id = 2 menjadi Alpukat dan stoknya berkurang 1. Di dalam command update tersebut kita juga menambahkan incremental update pada kolom version, sesuai algoritma di atas dan di-commit sehingga versionnya sekarang adalah 2.

id name stock expiring_date version
2 Alpukat 0 2023-01-30 2

Sesi A

UPDATE fruit
SET stock = stock - 1,
    version = version + 1
WHERE id = 2 AND 
      version = 1;

Kemudian Sesi A mengurangi stok Pisang sebanyak 1 buah dengan menambahkan where clause id dan version dengan value yang di-select sebelumnya, yaitu 1. Karena pada data tersebut Sesi B telah melakukan update dan mengganti versionnya menjadi 2, maka proses update pada Sesi A tidak dapat dilakukan sebab datanya sudah tidak ditemukan. Sekarang data tersebut tidak dapat lagi diubah secara serentak dan juga terhindar dari Write Skew & Lost Updates😎.

Optimistic Locking Handling

Setelah menerapkan Optimistic Locking lewat salah satu cara di atas, Update Anomaly dapat dihindari, baik pada Sesi A maupun Sesi B. Kita dapat menentukan algoritma selanjutnya pada aplikasi. Kita bisa melakukan throw Error ketika datanya tidak ditemukan karena valuenya sudah berubah untuk memberitahu user bahwa telah terjadi perubahan pada data yang sama secara bersamaan. Atau bisa juga dengan mengulang kembali sesi tersebut dan melakukan command update yang sama. Semuanya dikembalikan lagi pada masing-masing requirement bisnis tersebut. Contohnya pada data di atas kita dapat mengulangi kembali Sesi A dan melakukan validasi, jika stoknya 0 maka akan throw Error sehingga kita dapat mencegah jumlah stok menjadi minus.

Perlu diperhatikan bahwa Optimistic Locking ada performance cost karena mewajibkan kita melakukan query select before update untuk mendapatkan value sebelumnya pada sesi tersebut yang hasilnya nanti akan digunakan sebagai where clause saat update. Optimistic Locking menggunakan Version juga hanya bisa di-handle lewat aplikasi. Kalau ada yang melakukan update langsung ke database server tanpa melakukan increment version, tentu ga bakal kedetek perubahannya oleh aplikasi di saat bersamaan. Sedangkan Pessimistic Locking bisa di-handle baik lewat aplikasi maupun lewat database server langsung. Jadi sebenarnya ga ada solusi yang maha sempurna😅.

Verdict

Kita telah mempraktekkan cara mencegah Update Anomaly (Write Skew & Lost Updates) menggunakan Pessimistic Locking & Optimistic Locking. Pessimistic Locking itu mengunci menggunakan fitur di database. Sedangkan Optimistic Locking itu mengunci menggunakan algoritma di aplikasi. Ketika menggunakan Pessimistic Locking database akan menghandlenya dengan cara memblok proses transaksi pada sesi lain sesuai fitur dari database yang digunakan. Sedangkan pada Optimistic Locking kita bebas menentukan cara handlenya, entah itu akan throw Error atau mengulangi kembali sesinya dengan command yang sama. Optimistic Locking menggunakan Version Column adalah metode yang paling populer karena command updatenya lebih simple. Selain itu, implementasi fitur masing-masing database itu berbeda. Kalau kita mempercayakan proses locking pada fitur database, aplikasi jadi tidak tahan perubahan. Misalkan awalnya kita menggunakan Serializable Isolation pada PostgreSql yang menerapkan Snapshot Serializable. Lalu, kita migrasi ke MySql yang menerapkan 2 Phase Locking Serializable. Behaviornya jadi berbeda, yang sebelumnya mengunci dengan memblok update pada sesi lain berubah menjadi mengunci dengan cara menunggu sesi lain selesai. Hal itu bisa saja tidak sesuai dengan requirement bisnis. Oleh karena itu, Optimistic Locking dianggap paling cocok untuk sebagian besar kasus karena meskipun nantinya kita migrasi ke database lain behaviornya ga akan berubah karena dihandle oleh algoritma yang kita tulis di aplikasi. Jika menggunakan Hibernate pada Java, kita bisa menerapkan Optimistic Locking dengan mudah menggunakan annotasi @OptimisticLocking pada class entity dengan value type ALL untuk metode All Column. Atau menambahkan annotasi @Version pada property version di class entity untuk metode Version Column. Jadi ga perlu repot-repot bikin algoritma manual. Oh ya, ini bukan berarti Optimistic Locking merupakan solusi paling sempurna, karena Optimistic Locking tidak bisa dilakukan tanpa selection terlebih dahulu pada sesi tersebut. Tentu itu akan sedikit berdampak pada performance. Selain itu Optimistic Locking hanya bisa di-handle lewat aplikasi. Sehingga kalau ada yang melakukan update langsung di database server tentu ga ngaruh pengunciannya. Beda halnya dengan Pessimistic Locking yang bisa di-handle lewat aplikasi maupun lewat database server langsung. Tapi menurut gw Optimistic Locking masih worth it dibanding Pessimistic Locking, baik dari segi performance maupun dari segi maintanance. Semuanya dikembalikan lagi ke masing-masing engineer dan tergantung kasus yang dihadapi. Seperti yang pernah gw bilang, ga ada yang namanya silver bullet dalam pemrograman, semuanya ada cost dan benefitnya. Tinggal cari jalan tengah yang cocok aja😀.