Ringkasan tingkat isolasi

Halaman ini memperkenalkan berbagai tingkat isolasi dan menjelaskan cara kerjanya di Spanner.

Tingkat isolasi adalah properti database yang menentukan data mana yang terlihat oleh transaksi serentak. Spanner mendukung dua tingkat isolasi yang ditentukan dalam standar ANSI/ISO SQL: serialisabel dan repeatable read. Saat membuat transaksi, Anda harus memilih tingkat isolasi yang paling sesuai untuk transaksi tersebut. Tingkat isolasi yang dipilih memungkinkan setiap transaksi memprioritaskan berbagai faktor seperti latensi, tingkat pembatalan, dan apakah aplikasi rentan terhadap efek anomali data. Pilihan terbaik bergantung pada permintaan spesifik workload.

Isolasi serialisabel

Isolasi serialisabel adalah tingkat isolasi default di Spanner. Dalam isolasi serialisabel, Spanner memberi Anda jaminan kontrol konkurensi yang paling ketat untuk transaksi, yang disebut konsistensi eksternal. Spanner berperilaku seolah-olah semua transaksi dieksekusi secara berurutan, meskipun Spanner sebenarnya menjalankannya di beberapa server (dan mungkin di beberapa pusat data) untuk performa dan ketersediaan yang lebih tinggi daripada database server tunggal. Selain itu, jika satu transaksi selesai sebelum transaksi lain mulai di-commit, Spanner menjamin bahwa klien selalu melihat hasil transaksi dalam urutan berurutan. Secara intuitif, Spanner mirip dengan database mesin tunggal.

Sebagai gantinya, Spanner mungkin membatalkan transaksi jika workload memiliki pertentangan baca-tulis yang tinggi, yang menyebabkan banyak transaksi membaca data yang sedang diupdate oleh transaksi lain, karena sifat dasar transaksi serialisabel. Namun, ini adalah default yang baik untuk database operasional. Hal ini membantu Anda menghindari masalah waktu yang rumit yang biasanya hanya muncul dengan konkurensi tinggi. Masalah ini sulit direproduksi dan dipecahkan. Oleh karena itu, isolasi serialisabel memberikan perlindungan terkuat terhadap anomali data. Jika transaksi perlu dicoba lagi, mungkin ada peningkatan latensi karena percobaan ulang transaksi.

Isolasi repeatable read

Di Spanner, isolasi repeatable read diimplementasikan menggunakan teknik yang umumnya dikenal sebagai snapshot isolation. Isolasi repeatable read di Spanner memastikan bahwa semua operasi baca dalam transaksi melihat snapshot database yang konsisten, atau kuat, seperti yang ada pada awal transaksi. Hal ini juga menjamin bahwa penulisan serentak pada data yang sama hanya berhasil jika tidak ada konflik. Pendekatan ini bermanfaat dalam skenario konflik baca-tulis tinggi saat banyak transaksi membaca data yang mungkin diubah oleh transaksi lain. Dengan menggunakan snapshot tetap, repeatable read menghindari dampak performa dari tingkat isolasi serialisabel yang lebih ketat.

Dengan default konkurensi optimis, operasi baca dieksekusi tanpa memperoleh kunci dan tanpa memblokir penulisan serentak, yang menghasilkan lebih sedikit transaksi yang dibatalkan yang mungkin perlu dicoba lagi karena potensi konflik serialisasi. Dengan konkurensi pesimis, operasi baca menggunakan snapshot, tetapi kunci eksklusif berlaku untuk data yang dibaca dari FOR UPDATE kueri atau lock_scanned_ranges=exclusive petunjuk, dan data yang ditulis dengan kueri DML.

Untuk workload yang dimigrasikan dari database lain, sebaiknya konfigurasikan aplikasi Anda untuk menggunakan isolasi repeatable read di Spanner. Semantik transaksi repeatable read, khususnya penguncian untuk operasi baca, cocok dengan tingkat isolasi default di sebagian besar database lain (misalnya, MySQL dan PostgreSQL). Hal ini membantu mengurangi kebutuhan untuk mendesain ulang aplikasi Anda agar berfungsi dengan tingkat isolasi serialisabel default Spanner.

Tidak seperti isolasi serialisabel, repeatable read dapat menyebabkan anomali data jika aplikasi Anda mengandalkan hubungan atau batasan data tertentu yang tidak diterapkan oleh skema database, terutama saat urutan operasi penting. Dalam kasus seperti itu, transaksi dapat membaca data, membuat keputusan berdasarkan data tersebut, lalu menulis perubahan yang melanggar batasan khusus aplikasi tersebut, meskipun batasan skema database masih terpenuhi. Hal ini terjadi karena isolasi repeatable read memungkinkan transaksi serentak berlanjut tanpa serialisasi yang ketat. Satu potensi anomali dikenal sebagai write skew, yang muncul dari jenis update serentak tertentu, yang setiap update diterima secara independen, tetapi efek gabungannya melanggar integritas data aplikasi. Misalnya, bayangkan ada sistem rumah sakit yang mewajibkan setidaknya satu dokter untuk siaga setiap saat, dan dokter dapat meminta untuk tidak siaga selama shift. Dalam isolasi repeatable read, jika Dr. Richards dan Dr. Smith dijadwalkan untuk siaga pada shift yang sama dan secara serentak mencoba meminta untuk tidak siaga, setiap permintaan akan berhasil secara paralel. Hal ini karena kedua transaksi membaca bahwa ada setidaknya satu dokter lain yang dijadwalkan untuk siaga pada awal transaksi, yang menyebabkan anomali data jika transaksi berhasil. Di sisi lain, penggunaan isolasi serialisabel mencegah transaksi ini melanggar batasan karena transaksi serialisabel akan mendeteksi potensi anomali data dan membatalkan transaksi. Dengan demikian, konsistensi aplikasi akan dijamin dengan menerima tingkat pembatalan yang lebih tinggi.

Pada contoh sebelumnya, Anda dapat menggunakan klausa SELECT FOR UPDATE dalam isolasi repeatable read. Klausa SELECT ... FOR UPDATE memverifikasi apakah data yang dibaca pada snapshot yang dipilih tetap tidak berubah pada waktu commit. Demikian pula, pernyataan DML dan mutasi, yang membaca data secara internal untuk memastikan integritas penulisan, juga memverifikasi bahwa data tetap tidak berubah pada waktu commit. Selain itu, dengan konkurensi pesimis, data yang dibaca oleh SELECT ... FOR UPDATE, dan data yang ditulis oleh pernyataan DML memperoleh kunci eksklusif untuk mencegah transaksi mendatang melakukan modifikasi yang bertentangan sebelum transaksi saat ini di-commit.

Untuk workload yang dimigrasikan dari database lain yang menggunakan kueri FOR UPDATE, sebaiknya konfigurasikan aplikasi Anda untuk menggunakan isolasi repeatable read dengan konkurensi pesimis di Spanner. Aplikasi terus memperoleh kunci untuk data yang dibaca oleh SELECT ... FOR UPDATE, yang merupakan perilaku default di database lain.

Untuk mengetahui informasi selengkapnya, lihat Menggunakan isolasi repeatable read.

Contoh kasus penggunaan

Contoh berikut menunjukkan manfaat penggunaan isolasi repeatable read untuk menghilangkan overhead penguncian. Transaction 1 dan Transaction 2 berjalan dalam isolasi repeatable read.

Transaction 1 menetapkan stempel waktu snapshot saat pernyataan SELECT berjalan.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

/*-----------+------------------*
| AlbumId    | MarketingBudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

/*-----------+------------------*
| albumid    | marketingbudget  |
+------------+------------------+
| 1          | 50000            |
| 2          | 100000           |
| 3          | 70000            |
| 4          | 80000            |
*------------+------------------*/

Kemudian, Transaction 2 menetapkan stempel waktu snapshot setelah Transaction 1 dimulai, tetapi sebelum di-commit. Karena Transaction 1 belum mengupdate data, kueri SELECT di Transaction 2 membaca data yang sama dengan Transaction 1.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 berlanjut setelah Transaction 2 di-commit.

GoogleSQL

-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;

/*-----------*
| UsedBudget |
+------------+
| 300000     |
*------------*/

PostgreSQL

-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;

/*-----------*
| usedbudget |
+------------+
| 300000     |
*------------*/

Nilai UsedBudget yang ditampilkan Spanner adalah jumlah anggaran yang dibaca oleh Transaction 1. Jumlah ini hanya mencerminkan data yang ada di snapshot T1. Jumlah ini tidak menyertakan anggaran yang ditambahkan Transaction 2, karena Transaction 2 di-commit setelah Transaction 1 menetapkan snapshot T1. Menggunakan repeatable read berarti Transaction 1 tidak perlu dibatalkan meskipun Transaction 2 mengubah data yang dibaca oleh Transaction 1. Namun, hasil yang ditampilkan Spanner mungkin atau mungkin tidak sesuai dengan hasil yang diinginkan.

Konflik baca-tulis dan kebenaran

Pada contoh sebelumnya, jika data yang dikueri oleh pernyataan SELECT di Transaction 1 digunakan untuk membuat keputusan anggaran pemasaran berikutnya, mungkin ada masalah kebenaran.

Misalnya, asumsikan ada total anggaran 400,000. Berdasarkan hasil dari pernyataan SELECT di Transaction 1, kita mungkin mengira ada 100,000 yang tersisa dalam anggaran dan memutuskan untuk mengalokasikannya semua ke AlbumId = 4.

GoogleSQL

-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;

COMMIT;

PostgreSQL

-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;

COMMIT;

Transaction 1 berhasil di-commit, meskipun Transaction 2 telah mengalokasikan 50,000 dari sisa anggaran 100,000 ke album baru AlbumId = 5.

Anda dapat menggunakan sintaksis SELECT...FOR UPDATE untuk memvalidasi bahwa pembacaan transaksi tertentu tidak berubah selama masa aktif transaksi guna menjamin kebenaran transaksi. Pada contoh berikut menggunakan SELECT...FOR UPDATE, Transaction 1 dibatalkan pada waktu commit.

GoogleSQL

-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;

/*-----------*
| TotalBudget |
+------------+
| 300000     |
*------------*/

COMMIT;

PostgreSQL

-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;

/*-------------*
 | totalbudget |
 +-------------+
 | 300000      |
 *-------------*/

COMMIT;

Untuk mengetahui informasi selengkapnya, lihat Menggunakan SELECT FOR UPDATE dalam isolasi repeatable read.

Anda juga dapat menggunakan konkurensi pesimis, yang memperoleh kunci eksklusif pada data yang dibaca oleh pernyataan SELECT...FOR UPDATE. Misalnya, Transaction 1 dibatalkan pada waktu commit karena Transaction 2 meng-commit modifikasinya sebelum Transaction 1 memperoleh kunci, yang menyebabkan konflik. Namun, jika pengurutan transaksi menyebabkan Transaction 2 mencoba mengupdate anggaran pemasaran setelah Transaction 1 memperoleh kunci, Transaction 2 akan menunggu Transaction 1 di-commit dan melepaskan kunci sebelum dapat melanjutkan. Opsi konkurensi pesimis melakukan serialisasi akses ke data.

Untuk mengetahui informasi selengkapnya, lihat Kontrol konkurensi.

Konflik tulis-tulis dan kebenaran

Dengan menggunakan tingkat isolasi repeatable read, penulisan serentak pada data yang sama hanya berhasil jika tidak ada konflik.

Pada contoh berikut, Transaction 1 menetapkan stempel waktu snapshot pada pernyataan SELECT pertama.

GoogleSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

PostgreSQL

-- Transaction 1
BEGIN;

-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

Transaction 2 berikut membaca data yang sama dengan Transaction 1 dan menyisipkan item baru. Transaction 2 berhasil di-commit tanpa menunggu atau dibatalkan.

GoogleSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;

INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);

COMMIT;

PostgreSQL

-- Transaction 2
BEGIN;

-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;

INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);

COMMIT;

Transaction 1 berlanjut setelah Transaction 2 di-commit.

GoogleSQL

-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

PostgreSQL

-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;

Transaction 1 dibatalkan karena Transaction 2 telah meng-commit penyisipan ke baris AlbumId = 5.

Langkah berikutnya