การแยกธุรกรรมพร้อมกัน - เหตุใดฉันจึงสามารถอัปเดตชุดย่อยของบันทึกธุรกรรมอื่นได้

ฉันกำลังพยายามทำความเข้าใจปัญหาที่ฉันพบซึ่งฉันไม่เชื่อว่าควรจะเป็นไปได้เมื่อต้องจัดการกับธุรกรรมโดยใช้ระดับการแยกการอ่านที่คอมมิต ฉันมีโต๊ะที่ใช้เป็นคิว ในหนึ่งเธรด (การเชื่อมต่อ 1) ฉันแทรกชุดข้อมูล 20 รายการหลายชุดลงในแต่ละตาราง แต่ละชุดข้อมูล 20 รายการจะดำเนินการภายในธุรกรรม ในเธรดที่สอง (การเชื่อมต่อ 2) ฉันทำการอัพเดตเพื่อเปลี่ยนสถานะของเรคคอร์ดที่ถูกแทรกลงในคิว ซึ่งเกิดขึ้นภายในธุรกรรมด้วย เมื่อทำงานพร้อมกัน ฉันคาดหวังว่าจำนวนแถวที่ได้รับผลกระทบจากการอัปเดต (การเชื่อมต่อ 2) ควรเป็นผลคูณของ 20 เนื่องจากการเชื่อมต่อ 1 กำลังแทรกแถวในตารางที่แทรกเป็นชุด 20 แถวภายในธุรกรรม

แต่การทดสอบของฉันแสดงให้เห็นว่าไม่ได้เป็นเช่นนั้นเสมอไป และในบางครั้ง ฉันสามารถอัปเดตชุดย่อยของบันทึกจากชุดของการเชื่อมต่อ 1 ได้ สิ่งนี้ควรเป็นไปได้หรือฉันพลาดบางอย่างเกี่ยวกับธุรกรรม การทำงานพร้อมกัน และระดับการแยกออกจากกัน ด้านล่างนี้คือชุดสคริปต์ทดสอบที่ฉันสร้างขึ้นเพื่อจำลองปัญหานี้ใน T-SQL

สคริปต์นี้จะแทรก 20,000 บันทึกลงในตารางในชุดธุรกรรม 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'

สคริปต์นี้อัปเดตเรกคอร์ดในตาราง เปลี่ยนสถานะจาก 1 เป็น 2 จากนั้นตรวจสอบจำนวนแถวจากการดำเนินการอัปเดต เมื่อจำนวนแถวไม่เป็นจำนวนทวีคูณของ 20 และคำสั่งการพิมพ์จะระบุสิ่งนี้และจำนวนแถวที่ได้รับผลกระทบ

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'

สคริปต์นี้สร้างตารางคิวการทดสอบในฐานข้อมูลใหม่ที่เรียกว่า 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



คำตอบ (2)


ความคาดหวังของคุณผิดที่ผิดทางอย่างสิ้นเชิง คุณไม่เคยแสดงความต้องการในการ 'ถอนคิว' 20 แถวในข้อความค้นหาของคุณเลย UPDATE สามารถส่งคืนแถว 0, 19, 20, 21 หรือ 1,000 แถว และผลลัพธ์ทั้งหมดถูกต้อง ตราบใดที่ status คือ 1 และ type คือ 1 หากคุณคาดว่า 'dequeue' จะเกิดขึ้นตามลำดับของ 'enqueue' ( ซึ่งมีการหลบเลี่ยงไปในคำถามของคุณ แต่ไม่เคยระบุไว้อย่างชัดเจน) ดังนั้นการดำเนินการ 'dequeue' ของคุณจะต้องมีส่วนคำสั่ง ORDER BY หากคุณเพิ่มข้อกำหนดที่ระบุไว้อย่างชัดเจนแล้วความคาดหวังของคุณที่ 'dequeue' ส่งคืนแถว 'enqueue' ทั้งชุดเสมอ (เช่นหลาย ๆ 20 แถว) จะเข้าใกล้การเป็นความคาดหวังที่สมเหตุสมผลเพียงขั้นตอนเดียว อย่างที่ฉันพูดไปแล้ว อย่างที่สิ่งต่าง ๆ เป็นอยู่ตอนนี้ มันผิดที่ผิดทางโดยสิ้นเชิง

สำหรับการสนทนาที่ยาวกว่านี้ โปรดดูที่ การใช้ตารางเป็นคิว

ฉันไม่ควรกังวลว่าแม้ว่าธุรกรรมหนึ่งจะกระทำชุดบันทึกที่แทรกไว้ 20 รายการ แต่ธุรกรรมที่เกิดขึ้นพร้อมกันอีกรายการหนึ่งสามารถอัปเดตชุดย่อยของบันทึกเหล่านั้นได้เท่านั้น ไม่ใช่ทั้งหมด 20 รายการ

โดยพื้นฐานแล้ว คำถามจะอยู่ที่ หากฉัน SELECT ขณะที่ฉัน INSERT ฉันจะเห็นแถวที่แทรกไว้กี่แถว คุณมีสิทธิ์เท่านั้นที่จะกังวลหากมีการประกาศระดับการแยกเป็นอนุกรม ระดับการแยกอื่นๆ ไม่มีการคาดการณ์ใดๆ เกี่ยวกับจำนวนแถวที่ แทรกในขณะที่ UPDATE กำลังทำงานอยู่ จะปรากฏให้เห็น มีเพียง SERIALIZABLE เท่านั้นที่ระบุว่าผลลัพธ์จะต้องเหมือนกับการรันสองคำสั่งทีละรายการ (เช่น ทำให้เป็นอนุกรม จึงเป็นที่มาของชื่อ) แม้ว่ารายละเอียดทางเทคนิค อย่างไร ที่ UPDATE 'เห็น' เพียงส่วนหนึ่งของชุด INSERT นั้นง่ายต่อการเข้าใจเมื่อคุณพิจารณาลำดับทางกายภาพและไม่มี ORDER BY clause คำอธิบายก็ไม่เกี่ยวข้อง ปัญหาพื้นฐานคือความคาดหวังนั้นไม่รับประกัน แม้ว่า 'ปัญหา' จะถูก 'แก้ไข' ด้วยการเพิ่ม ORDER BY ที่เหมาะสมและคีย์ดัชนีคลัสเตอร์ที่ถูกต้อง (บทความที่ลิงก์ด้านบนอธิบายรายละเอียด) ความคาดหวังก็ยังไม่รับประกัน จะยังคงถูกต้องตามกฎหมายอย่างสมบูรณ์สำหรับ UPDATE ที่จะ 'ดู' 1, 19 หรือ 21 แถว แม้ว่าจะไม่น่าจะเกิดขึ้นก็ตาม

ฉันเดาว่าฉันเข้าใจมาโดยตลอดว่า READ COMMITTED จะอ่านเฉพาะข้อมูลที่คอมมิตเท่านั้น และการคอมมิตธุรกรรมนั้นเป็นการดำเนินการแบบอะตอมมิก ทำให้การเปลี่ยนแปลงทั้งหมดที่เกิดขึ้นในธุรกรรมพร้อมใช้งานในคราวเดียว

ถูกต้อง. สิ่งที่ไม่ถูกต้องคือการคาดหวังว่า พร้อมกัน SELECT (หรืออัปเดต) เพื่อดูการเปลี่ยนแปลงทั้งหมด โดยไม่เกี่ยวข้องกับตำแหน่งที่เกิดขึ้นในการดำเนินการ เปิดแบบสอบถาม SSMS และเรียกใช้สิ่งต่อไปนี้:

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)

ตอนนี้เปิดแบบสอบถาม SSMS ใหม่และเรียกใช้สิ่งต่อไปนี้:

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

สิ่งนี้จะบล็อกอยู่หลัง INSERT ที่ไม่มีข้อผูกมัด ตอนนี้กลับไปที่แบบสอบถามแรกและเรียกใช้สิ่งต่อไปนี้:

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

เมื่อคอมมิตนี้ แบบสอบถาม SSMS ที่สองจะเสร็จสิ้น และจะส่งกลับสองแถว ไม่ใช่สามแถว ถาม นี่คือการอ่านที่มุ่งมั่น สิ่งที่คุณคาดหวังคือการดำเนินการแบบอนุกรม (ซึ่งในกรณีนี้ตัวอย่างข้างต้นจะหยุดชะงัก)

person Remus Rusanu    schedule 23.04.2012
comment
ในตัวอย่างของฉัน ฉันจะลบออกจากคิวเท่านั้น บันทึกที่ได้รับการอัพเดตโดยการเชื่อมต่อเดียวกันกับสถานะ = 2 แต่ให้ลบการลบออกจากโค้ดเพื่อทำให้ง่ายขึ้น ข้อกังวลเดิมของฉันยังคงมีอยู่ ซึ่งเป็นความจริงที่ว่าคำสั่งอัปเดต ส่งคืนจำนวนแถวที่ไม่เป็นผลคูณของ 20 ซึ่งเป็นขนาดแบตช์ของธุรกรรมที่เกิดขึ้นพร้อมกันซึ่งกำลังแทรกบันทึกลงในฐานข้อมูล - person A. Barton; 24.04.2012
comment
ข้อเท็จจริงที่ว่าคุณแยกการอัปเดตออกจากการลบนั้นไม่เกี่ยวข้อง ความกังวลของคุณยังไม่มีมูลความจริงเลย - person Remus Rusanu; 24.04.2012
comment
ดังนั้น ฉันไม่ควรกังวลว่าแม้ว่าธุรกรรมหนึ่งจะกระทำชุดบันทึกที่แทรกไว้ 20 รายการ แต่ธุรกรรมที่เกิดขึ้นพร้อมกันอีกรายการหนึ่งสามารถอัปเดตชุดย่อยของบันทึกเหล่านั้นได้เท่านั้น ไม่ใช่ทั้งหมด 20 รายการ โปรดอธิบาย ฉันต้องการที่จะเข้าใจว่าสิ่งนี้เป็นไปได้อย่างไร - person A. Barton; 24.04.2012
comment
ขอบคุณสำหรับการอัพเดท. ฉันเดาว่าฉันเข้าใจมาโดยตลอดว่า READ COMMITTED จะอ่านเฉพาะข้อมูลที่คอมมิตเท่านั้น และการคอมมิตธุรกรรมนั้นเป็นการดำเนินการแบบอะตอมมิก ทำให้การเปลี่ยนแปลงทั้งหมดที่เกิดขึ้นในธุรกรรมพร้อมใช้งานในคราวเดียว - person A. Barton; 24.04.2012
comment
น่าสนใจมาก. ดังนั้นเนื่องจากการแทรกแถวก่อนแถวบนสุดตามดัชนีคลัสเตอร์ (คอลัมน์ a) แต่หลังจากการอัพเดตถูกบล็อก การอัพเดตจะไม่ส่งผลกระทบต่อแถว แต่ถ้าแถวที่แทรกมาหลังแถวบนสุด แถวนั้นจะถูกบล็อก ปรับปรุงแล้ว เมื่อใช้ข้อมูลนั้น ฉันควรเปลี่ยนดัชนีคลัสเตอร์ตารางของฉันเป็น Transaction_id, command_id ซึ่งเพิ่มตัวระบุที่แทรกตามลำดับจากน้อยไปมากลงในตาราง - person A. Barton; 25.04.2012
comment
สิ่งนี้จะช่วยให้มั่นใจได้ว่าการบล็อกการอัปเดตจะเกิดขึ้นในแถวแรกของชุดงาน และจะรอจนกว่าชุดงานจะเสร็จสมบูรณ์ โดยอนุญาตให้อัปเดตทั้ง 20 แถวในชุดงานได้ - person A. Barton; 25.04.2012
comment
คุณต้องเพิ่มส่วนคำสั่ง ORDER BY ใน UPDATE ของคุณด้วย - person Remus Rusanu; 25.04.2012

มันอาจจะเกิดขึ้นเช่นนี้:

  1. ตัวเขียน/ตัวแทรกเขียน 20 แถว (ไม่คอมมิต)
  2. เครื่องอ่าน/ตัวอัปเดตอ่านหนึ่งแถว (ซึ่งไม่ได้คอมมิต - จะละทิ้ง)
  3. ผู้เขียน/ผู้แทรกกระทำ
  4. เครื่องอ่าน/ตัวอัปเดตอ่าน 19 แถวซึ่งขณะนี้คอมมิตแล้วจึงมองเห็นได้

ฉันเชื่อว่ามีเพียงระดับการแยกของการแยกแบบอนุกรมได้ (หรือการแยกสแน็ปช็อตซึ่งเกิดขึ้นพร้อมกันมากกว่า) เท่านั้นที่จะแก้ไขปัญหานี้ได้

person usr    schedule 23.04.2012
comment
เหตุใดผู้อ่าน / ผู้อัพเดตจึงอ่านหนึ่งแถวในขั้นตอนที่ 2 หากระดับการแยกถูกยอมรับในการอ่าน ฉันเข้าใจว่าเมื่อตั้งค่าระดับการแยกเป็น READ COMMITTED การเปลี่ยนแปลงที่เกิดขึ้นในธุรกรรมที่ไม่มีข้อผูกมัดไม่ควรมองเห็นหรืออ่านได้โดยธุรกรรมภายนอก - person A. Barton; 23.04.2012
comment
ใช่ พวกมันไม่สามารถมองเห็นได้ แต่ก็มีอยู่บนดิสก์ ดังนั้นจึงต้องอ่านเพื่อให้สามารถสแกนตารางล่วงหน้าได้! คุณแค่ไม่ได้เห็นพวกเขาเพราะพวกเขาถูกทิ้งทันทีในการสแกน - person usr; 23.04.2012
comment
แต่ในกรณีนั้น ฉันคาดหวังว่าจำนวนแถวจากการอัพเดตจะเป็นศูนย์ และเมื่อมีการออกคอมมิตโดยเธรดอื่น จำนวนแถวการอัปเดตควรส่งคืน 20 สำหรับธุรกรรม ควรเป็นทั้งหมดหรือไม่มีเลยเท่าที่สามารถทำได้ อ่าน/อัพเดตบันทึกจากการทำธุรกรรมครั้งแรกถูกต้องหรือไม่? - person A. Barton; 23.04.2012
comment
การอัพเดตมีลำดับสองขั้นตอน: อ่านแถวที่จะอัพเดตและอัพเดต สองขั้นตอนนี้ไม่ได้เกิดขึ้นแบบอะตอมมิก! มาทำการทดลองทางความคิดกัน: เรากำลังแทรกข้อมูลจำนวน 100TB เป็นกลุ่ม การดำเนินการขั้นสุดท้ายอยู่ระหว่างการพิจารณา เซสชันการอัปเดตจะเริ่มอ่านแถวที่จะอัปเดต การสแกนตารางจะใช้เวลาสักครู่ (ชั่วโมง) ระหว่างทางที่เซสชันการแทรกกระทำ ขณะนี้แถวทั้งหมด 100TB สามารถมองเห็นได้ในทันที/แบบอะตอมมิก แต่เซสชันการอ่านจะเห็นเพียงบางส่วนเท่านั้น การอ่านที่คอมมิตไม่ได้จัดเตรียมอะตอมมิกซิตีในกรณีที่เกิดพร้อมกัน! เรื่องความทนทานเท่านั้น มีความแตกต่าง - person usr; 24.04.2012