Isolasi Konkurensi Transaksi - Mengapa saya dapat memperbarui subset catatan transaksi lain?

Saya mencoba memahami masalah yang saya alami yang menurut saya tidak mungkin terjadi ketika menangani transaksi menggunakan tingkat isolasi komitmen baca. Saya memiliki tabel yang digunakan sebagai antrian. Dalam satu utas (koneksi 1) saya memasukkan beberapa kumpulan 20 catatan ke dalam setiap tabel. Setiap batch yang terdiri dari 20 catatan dilakukan di dalam suatu transaksi. Di thread kedua (koneksi 2) saya melakukan update untuk mengubah status record yang telah dimasukkan ke dalam antrian, yang juga terjadi di dalam suatu transaksi. Saat dijalankan secara bersamaan, saya berharap jumlah baris yang terpengaruh oleh pembaruan (koneksi 2) harus kelipatan 20, karena koneksi 1 menyisipkan baris ke dalam tabel yang disisipkan dalam kumpulan 20 baris dalam suatu transaksi.

Namun pengujian saya menunjukkan bahwa hal ini tidak selalu terjadi, dan kadang-kadang saya dapat memperbarui subset catatan dari kumpulan koneksi 1. Apakah ini mungkin terjadi atau saya melewatkan sesuatu tentang transaksi, konkurensi, dan tingkat isolasi? Di bawah ini adalah serangkaian skrip pengujian yang saya buat untuk mereproduksi masalah ini di T-SQL.

Skrip ini menyisipkan 20.000 catatan ke dalam tabel dalam kumpulan transaksi sebanyak 20.

USE ReadTest
GO

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO

SET NOCOUNT ON

DECLARE @trans_id INTEGER
DECLARE @cmd_id INTEGER
DECLARE @text_str VARCHAR(4000)

SET @trans_id = 0
SET @text_str = 'Placeholder String Value'                

-- First empty the table
DELETE FROM TABLE_A

WHILE @trans_id < 1000 BEGIN
    SET @trans_id = @trans_id + 1
    SET @cmd_id = 0

    BEGIN TRANSACTION
--  Insert 20 records into the table per transaction
    WHILE @cmd_id < 20 BEGIN
        SET @cmd_id = @cmd_id + 1

        INSERT INTO TABLE_A ( transaction_id, command_id, [type], status, text_field ) 
            VALUES ( @trans_id, @cmd_id, 1, 1,  @text_str )
    END             
    COMMIT

END

PRINT 'DONE'

Skrip ini memperbarui catatan dalam tabel, mengubah status dari 1 menjadi 2 dan kemudian memeriksa jumlah baris dari operasi pembaruan. Jika jumlah baris bukan kelipatan 20, dan pernyataan print menunjukkan hal ini dan jumlah baris yang terpengaruh.

USE ReadTest
GO

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO

SET NOCOUNT ON
DECLARE @loop_counter INTEGER
DECLARE @trans_id INTEGER
DECLARE @count INTEGER

SET @loop_counter = 0

WHILE @loop_counter < 100000 BEGIN

    SET @loop_counter = @loop_counter + 1
    BEGIN TRANSACTION
        UPDATE TABLE_A SET status = 2 
        WHERE status = 1
            and type = 1
        SET @count = @@ROWCOUNT
    COMMIT

    IF ( @count % 20 <> 0 ) BEGIN
--      Records in concurrent transaction inserting in batches of 20 records before commit.
        PRINT '*** Rowcount not a multiple of 20. Count = ' + CAST(@count AS VARCHAR) + ' ***'
    END

    IF @count > 0 BEGIN
--      Delete the records where the status was changed.
        DELETE TABLE_A WHERE status = 2
    END
END

PRINT 'DONE'

Skrip ini membuat tabel antrian pengujian di database baru bernama ReadTest.

USE master;
GO

IF EXISTS (SELECT * FROM sys.databases WHERE name = 'ReadTest')
  BEGIN;
  DROP DATABASE ReadTest;
  END;
GO

CREATE DATABASE ReadTest;
GO

ALTER DATABASE ReadTest
SET ALLOW_SNAPSHOT_ISOLATION OFF
GO

ALTER DATABASE ReadTest
SET READ_COMMITTED_SNAPSHOT OFF
GO

USE ReadTest
GO

CREATE TABLE [dbo].[TABLE_A](
    [ROWGUIDE] [uniqueidentifier] NOT NULL,
    [TRANSACTION_ID] [int] NOT NULL,
    [COMMAND_ID] [int] NOT NULL,
    [TYPE] [int] NOT NULL,
    [STATUS] [int] NOT NULL,
    [TEXT_FIELD] [varchar](4000) NULL
 CONSTRAINT [PK_TABLE_A] PRIMARY KEY NONCLUSTERED 
(
    [ROWGUIDE] ASC
) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[TABLE_A] ADD  DEFAULT (newsequentialid()) FOR [ROWGUIDE]
GO

person A. Barton    schedule 23.04.2012    source sumber


Jawaban (2)


Harapan Anda sepenuhnya salah. Anda belum pernah menyatakan dalam kueri Anda persyaratan untuk 'menghapus antrean' tepat 20 baris. UPDATE dapat mengembalikan 0, 19, 20, 21 atau 1000 baris dan semua hasilnya benar, selama status adalah 1 dan type adalah 1. Jika Anda mengharapkan 'dequeue' terjadi dalam urutan 'enqueue' ( yang entah bagaimana dihindari dalam pertanyaan Anda, tetapi tidak pernah dinyatakan secara eksplisit) maka operasi 'dequeue' Anda harus berisi klausa ORDER BY. Seandainya Anda menambahkan persyaratan yang dinyatakan secara eksplisit maka ekspektasi Anda bahwa 'dequeue' selalu mengembalikan seluruh kumpulan baris 'enqueue' (yaitu kelipatan 20 baris) akan selangkah lebih dekat untuk menjadi ekspektasi yang masuk akal. Saat ini, seperti yang saya katakan, semuanya salah tempat.

Untuk diskusi lebih panjang lihat Menggunakan Tabel sebagai Antrean.

Saya tidak perlu khawatir bahwa ketika satu transaksi melakukan kumpulan 20 catatan yang disisipkan, transaksi bersamaan lainnya hanya dapat memperbarui sebagian dari catatan tersebut dan tidak semuanya 20?

Pada dasarnya pertanyaannya adalah Jika saya PILIH sambil MASUKKAN, berapa banyak baris yang disisipkan yang akan saya lihat?. Anda hanya berhak khawatir jika tingkat isolasi dinyatakan sebagai SERIALIZABLE. Tak satu pun dari tingkat isolasi lainnya yang membuat prediksi tentang berapa banyak baris yang dimasukkan saat UPDATE berjalan akan terlihat. Hanya SERIALIZABLE yang menyatakan bahwa hasilnya harus sama dengan menjalankan dua pernyataan satu demi satu (yaitu berseri, maka namanya). Meskipun detail teknis tentang bagaimana UPDATE 'melihat' hanya sebagian dari kumpulan INSERT mudah dipahami setelah Anda mempertimbangkan urutan fisik dan kurangnya klausa ORDER BY, penjelasannya tidak relevan. Masalah mendasarnya adalah bahwa ekspektasi tersebut tidak dapat dijamin. Bahkan jika 'masalah' 'diperbaiki' dengan menambahkan ORDER BY yang tepat dan kunci indeks cluster yang benar (artikel yang ditautkan di atas menjelaskan detailnya), ekspektasi tersebut masih tidak bergaransi. Masih sah jika UPDATE 'melihat' 1, 19, atau 21 baris, meskipun kecil kemungkinannya akan terjadi.

Saya kira saya selalu memahami READ COMMITTED untuk hanya membaca data yang dikomit, dan bahwa komit transaksi adalah operasi atom, membuat semua perubahan yang terjadi dalam transaksi tersedia sekaligus.

Itu betul. Apa yang salah adalah mengharapkan SELECT (atau pembaruan) bersamaan untuk melihat seluruh perubahan, tidak peduli di mana perubahan itu terjadi dalam eksekusi. Buka kueri SSMS dan jalankan perintah berikut:

use tempdb;
go

create table test (a int not null primary key, b int);
go

insert into test (a, b) values (5,0)
go

begin transaction
insert into test (a, b) values (10,0)

Sekarang buka kueri SSMS baru dan jalankan perintah berikut:

update test 
    set b=1
    output inserted.*
    where b=0

Ini akan memblokir di belakang INSERT yang tidak dikomit. Sekarang kembali ke kueri pertama dan jalankan perintah berikut:

insert into test (a, b) values (1,0)
commit

Ketika ini dilakukan, kueri SSMS kedua akan selesai, dan akan mengembalikan dua baris, bukan tiga. QED. Ini BACA KOMITMEN. Apa yang Anda harapkan adalah eksekusi SERIALIZABLE (dalam hal ini contoh di atas akan menemui jalan buntu).

person Remus Rusanu    schedule 23.04.2012
comment
Dalam contoh saya, saya hanya menghapus dari antrian, catatan yang diperbarui oleh koneksi yang sama ke status = 2. Tapi mari kita hapus penghapusan dari kode untuk menyederhanakan, kekhawatiran awal saya masih ada, yaitu fakta bahwa pernyataan pembaruan mengembalikan jumlah baris yang bukan kelipatan 20, yang merupakan ukuran batch transaksi bersamaan yang memasukkan catatan ke dalam database. - person A. Barton; 24.04.2012
comment
Fakta bahwa Anda memisahkan pembaruan dari penghapusan tidak relevan. Kekhawatiran Anda masih belum berdasar sama sekali. - person Remus Rusanu; 24.04.2012
comment
Jadi saya tidak perlu khawatir bahwa ketika satu transaksi melakukan kumpulan 20 catatan yang disisipkan, transaksi bersamaan lainnya hanya dapat memperbarui sebagian dari catatan tersebut dan tidak semuanya 20? Tolong jelaskan, saya ingin memahami bagaimana hal ini bisa terjadi. - person A. Barton; 24.04.2012
comment
Terima kasih atas pembaruannya. Saya kira saya selalu memahami READ COMMITTED untuk hanya membaca data yang dikomit, dan bahwa komit transaksi adalah operasi atom, membuat semua perubahan yang terjadi dalam transaksi tersedia sekaligus. - person A. Barton; 24.04.2012
comment
Sangat menarik. Jadi karena suatu baris disisipkan sebelum baris atas berdasarkan indeks berkerumun (kolom a), tetapi setelah pembaruan diblokir, pembaruan tidak mempengaruhi baris tersebut, tetapi jika baris yang disisipkan muncul setelah baris atas, maka baris tersebut adalah diperbarui. Dengan menggunakan informasi itu, saya harus mengubah indeks cluster tabel saya menjadi Transaction_id, Command_id, yang merupakan pengidentifikasi yang semakin meningkat yang dimasukkan dalam urutan menaik ke dalam tabel. - person A. Barton; 25.04.2012
comment
Hal ini akan memastikan bahwa pemblokiran pembaruan terjadi pada baris pertama dalam kumpulan dan akan menunggu hingga kumpulan selesai, sehingga 20 baris dalam kumpulan dapat diperbarui. - person A. Barton; 25.04.2012
comment
Anda juga perlu menambahkan klausa ORDER BY ke UPDATE Anda. - person Remus Rusanu; 25.04.2012

Hal ini bisa terjadi seperti ini:

  1. Penulis/penyisip menulis 20 baris (tidak melakukan)
  2. Pembaca/pembaru membaca satu baris (yang tidak dikomit - ia membuangnya)
  3. Penulis/penyisip berkomitmen
  4. Pembaca/pembaru membaca 19 baris yang sekarang dikomit sehingga terlihat

Saya percaya bahwa hanya tingkat isolasi serializable (atau isolasi snapshot yang lebih bersamaan) yang dapat memperbaiki masalah ini.

person usr    schedule 23.04.2012
comment
Mengapa pembaca/pembaru membaca satu baris pada langkah 2 jika tingkat isolasi READ COMMITTED? Menurut pemahaman saya, dengan tingkat isolasi yang disetel ke READ COMMITTED, perubahan yang terjadi pada transaksi yang tidak dikomit tidak boleh terlihat/dapat dibaca oleh transaksi eksternal. - person A. Barton; 23.04.2012
comment
Ya, mereka tidak terlihat. Namun, mereka ada di disk sehingga harus dibaca untuk melanjutkan pemindaian tabel! Anda hanya tidak dapat melihatnya karena langsung dibuang saat pemindaian. - person usr; 23.04.2012
comment
Tetapi dalam hal ini saya mengharapkan jumlah baris dari pembaruan menjadi nol dan setelah komit dikeluarkan oleh utas lain, jumlah baris pembaruan harus mengembalikan 20. Untuk transaksi, harus semua atau tidak sama sekali sejauh bisa baca/perbarui catatan dari transaksi pertama, benar? - person A. Barton; 23.04.2012
comment
Pembaruan adalah rangkaian dua langkah: membaca baris yang akan diperbarui dan memperbaruinya. Kedua langkah ini tidak terjadi secara atomik! Mari kita lakukan eksperimen pemikiran: Kita memasukkan kumpulan data sebesar 100 TB. Komitmen terakhir masih tertunda. Sesi pembaruan mulai membaca baris yang akan diperbarui. Diperlukan waktu beberapa saat untuk memindai tabel (jam). Di tengah-tengah sesi penyisipan dilakukan. Sekarang semua baris 100TB tiba-tiba/terlihat secara atom namun sesi membaca hanya melihat sebagian saja. Komitmen baca tidak memberikan atomisitas jika terjadi konkurensi! Hanya soal daya tahan. Ada perbedaan. - person usr; 24.04.2012