SQL Server trả về cả giá trị mới và giá trị cũ như thế nào trong CẬP NHẬT?


8

Chúng tôi đã có vấn đề, trong khi đồng thời cao, các truy vấn trả về kết quả không nhạy cảm - kết quả là vi phạm logic của các truy vấn được đưa ra. Phải mất một thời gian để tái tạo vấn đề. Tôi đã quản lý để chắt lọc vấn đề có thể lặp lại xuống một vài T-SQL.

Lưu ý : Phần của hệ thống trực tiếp có sự cố bao gồm 5 bảng, 4 trình kích hoạt, 2 thủ tục được lưu trữ và 2 lượt xem. Tôi đã đơn giản hóa hệ thống thực thành một cái gì đó dễ quản lý hơn cho một câu hỏi được đăng. Mọi thứ đã được giảm xuống, các cột được loại bỏ, các thủ tục được lưu trữ được thực hiện nội tuyến, các khung nhìn biến thành các biểu thức bảng chung, các giá trị của các cột được thay đổi. Đây là tất cả một cách dài để nói rằng trong khi những gì tiếp theo tái tạo một lỗi, nó có thể khó hiểu hơn. Bạn sẽ phải kiềm chế tự hỏi tại sao một cái gì đó được cấu trúc theo cách nó được. Tôi ở đây đang cố gắng tìm hiểu tại sao tình trạng lỗi có thể lặp lại xảy ra trong mô hình đồ chơi này.

/*
The idea in this system is that people are able to take days off. 
We create a table to hold these *"allocations"*, 
and declare sample data that only **1** production operator 
is allowed to take time off:
*/
IF OBJECT_ID('Allocations') IS NOT NULL DROP TABLE Allocations
CREATE TABLE [dbo].[Allocations](
    JobName varchar(50) PRIMARY KEY NOT NULL,
    Available int NOT NULL
)
--Sample allocation; there is 1 avaialable slot for this job
INSERT INTO Allocations(JobName, Available)
VALUES ('Production Operator', 1);

/*
Then we open up the system to the world, and everyone puts in for time. 
We store these requests for time off as *"transactions"*. 
Two production operators requested time off. 
We create sample data, and note that one of the users 
created their transaction first (by earlier CreatedDate):
*/
IF OBJECT_ID('Transactions') IS NOT NULL DROP TABLE Transactions;
CREATE TABLE [dbo].[Transactions](
    TransactionID int NOT NULL PRIMARY KEY CLUSTERED,
    JobName varchar(50) NOT NULL,
    ApprovalStatus varchar(50) NOT NULL,
    CreatedDate datetime NOT NULL
)
--Two sample transactions
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (52625, 'Production Operator', 'Booked', '20140125 12:00:40.820');
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (60981, 'Production Operator', 'WaitingList', '20150125 12:19:44.717');

/*
The allocation, and two sample transactions are now in the database:
*/
--Show the sample data
SELECT * FROM Allocations
SELECT * FROM Transactions

Các giao dịch đều được chèn vào như WaitingList. Tiếp theo, chúng tôi có một nhiệm vụ định kỳ chạy, tìm kiếm các vị trí trống và đưa bất kỳ ai vào Danh sách chờ vào trạng thái Đã đặt.

Trong một cửa sổ SSMS riêng, chúng tôi có quy trình được lưu định kỳ mô phỏng:

/*
    Simulate recurring task that looks for empty slots, 
    and bumps someone on the waiting list into that slot.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

--DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 1000000)
BEGIN
    SET @attempts = @attempts+1;

    /*
        The concept is that if someone is already "Booked", then they occupy an available slot.
        We compare the configured amount of allocations (e.g. 1) to how many slots are used.
        If there are any slots leftover, then find the **earliest** created transaction that 
        is currently on the WaitingList, and set them to Booked.
    */

    PRINT '=== Looking for someone to bump ==='
    WITH AvailableAllocations AS (
        SELECT 
            a.JobName,
            a.Available AS Allocations, 
            ISNULL(Booked.BookedCount, 0) AS BookedCount, 
            a.Available-ISNULL(Booked.BookedCount, 0) AS Available
        FROM Allocations a
            FULL OUTER JOIN (
                SELECT t.JobName, COUNT(*) AS BookedCount
                FROM Transactions t
                WHERE t.ApprovalStatus IN ('Booked') 
                GROUP BY t.JobName
            ) Booked
            ON a.JobName = Booked.JobName
        WHERE a.Available > 0
    )
    UPDATE Transactions SET ApprovalStatus = 'Booked'
    WHERE TransactionID = (
        SELECT TOP 1 t.TransactionID
        FROM AvailableAllocations aa
            INNER JOIN Transactions t
            ON aa.JobName = t.JobName
            AND t.ApprovalStatus = 'WaitingList'
        WHERE aa.Available > 0
        ORDER BY t.CreatedDate 
    )


    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

Và cuối cùng chạy nó trong cửa sổ kết nối SSMS thứ 3. Điều này mô phỏng một vấn đề tương tranh trong đó giao dịch trước đó đi từ chiếm một vị trí, đến trong danh sách chờ:

/*
    Toggle the earlier transaction back to "WaitingList".
    This means there are two possibilies:
       a) the transaction is "Booked", meaning no slots are available. 
          Therefore nobody should get bumped into "Booked"
       b) the transaction is "WaitingList", 
          meaning 1 slot is open and both tranasctions are "WaitingList"
          The earliest transaction should then get "Booked" into the slot.

    There is no time when there is an open slot where the 
    first transaction shouldn't be the one to get it - he got there first.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 100000)
BEGIN
    SET @attempts = @attempts+1

    /*Flip the earlier transaction from Booked back to WaitingList
        Because it's now on the waiting list -> there is a free slot.
        Because there is a free slot -> a transaction can be booked.
        Because this is the earlier transaction -> it should always be chosen to be booked
    */
    --DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

    PRINT '=== Putting the earlier created transaction on the waiting list ==='

    UPDATE Transactions
    SET ApprovalStatus = 'WaitingList'
    WHERE TransactionID = 52625

    --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS

    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

Về mặt khái niệm, quy trình va chạm tiếp tục tìm kiếm bất kỳ vị trí trống nào. Nếu nó tìm thấy một, nó sẽ thực hiện giao dịch sớm nhất trên WaitingListvà đánh dấu nó là Booked.

Khi được kiểm tra mà không đồng thời, logic hoạt động. Chúng tôi có hai giao dịch:

  • 12:00 chiều: Danh sách chờ
  • 12:20 chiều: Danh sách chờ

Có 1 phân bổ và 0 giao dịch đã đặt, vì vậy chúng tôi đánh dấu giao dịch trước đó là đã đặt:

  • 12:00 chiều: Đã đặt trước
  • 12:20 chiều: Danh sách chờ

Lần sau khi tác vụ chạy, hiện có 1 vị trí được đưa lên - vì vậy không có gì để cập nhật.

Nếu sau đó chúng tôi cập nhật giao dịch đầu tiên và đưa nó vào WaitingList:

UPDATE Transactions SET ApprovalStatus='WaitingList'
WHERE TransactionID = 60981

Sau đó, chúng tôi trở lại nơi chúng tôi bắt đầu:

  • 12:00 chiều: Danh sách chờ
  • 12:20 chiều: Danh sách chờ

Lưu ý : Bạn có thể tự hỏi tại sao tôi lại đưa giao dịch vào danh sách chờ. Đó là một tai nạn của mô hình đồ chơi đơn giản hóa. Trong các giao dịch hệ thống thực sự có thể PendingApproval, cũng chiếm một vị trí. Giao dịch PendingApproval được đưa vào danh sách chờ khi được phê duyệt. Không quan trọng. Đừng lo lắng về nó.

Nhưng khi tôi giới thiệu đồng thời, bằng cách có một cửa sổ thứ hai liên tục đưa giao dịch đầu tiên trở lại danh sách chờ sau khi được đặt, sau đó giao dịch sau đó được quản lý để có được đặt phòng:

  • 12:00 chiều: Danh sách chờ
  • 12:20 chiều: Đã đặt trước

Các kịch bản thử nghiệm đồ chơi nắm bắt điều này và ngừng lặp lại:

Msg 50000, Level 16, State 1, Line 41
The later tranasction, that should never be booked, managed to get booked!

Tại sao?

Câu hỏi là, tại sao trong mô hình đồ chơi này, điều kiện giải cứu này lại được kích hoạt?

Có hai trạng thái có thể cho trạng thái phê duyệt giao dịch đầu tiên:

  • Đã đặt trước : trong trường hợp vị trí bị chiếm dụng và giao dịch sau này không thể có
  • Danh sách chờ : trong trường hợp đó có một vị trí trống và hai giao dịch muốn có. Nhưng vì chúng tôi luôn selectlà giao dịch lâu đời nhất (tức là ORDER BY CreatedDate) giao dịch đầu tiên sẽ có được nó.

Tôi nghĩ có lẽ vì các chỉ số khác

Tôi đã học được rằng sau khi CẬP NHẬT đã bắt đầu và dữ liệu đã được sửa đổi, có thể đọc các giá trị cũ. Trong điều kiện ban đầu:

  • Chỉ số cụm :Booked
  • Chỉ mục không phân cụm :Booked

Sau đó, tôi thực hiện cập nhật và trong khi nút lá chỉ mục được phân cụm đã được sửa đổi, mọi chỉ mục không được phân cụm vẫn chứa giá trị ban đầu và vẫn có sẵn để đọc:

  • Chỉ mục cụm (Khóa độc quyền):Booked WaitingList
  • Chỉ mục không phân cụm : (đã mở khóa)Booked

Nhưng điều đó không giải thích vấn đề quan sát được. Có, giao dịch không còn được đặt trước , có nghĩa là bây giờ có một vị trí trống. Nhưng sự thay đổi đó chưa được cam kết, nó vẫn được tổ chức độc quyền. Nếu thủ tục va chạm chạy, nó sẽ:

  • chặn: nếu tùy chọn cơ sở dữ liệu cách ly ảnh chụp tắt
  • đọc giá trị cũ (ví dụ Booked): nếu bật cách ly ảnh chụp nhanh

Dù bằng cách nào, công việc va chạm sẽ không biết có một khe trống.

Vì vậy, tôi không có ý tưởng

Chúng tôi đã đấu tranh trong nhiều ngày để tìm hiểu làm thế nào những kết quả vô nghĩa này có thể xảy ra.

Bạn có thể không hiểu hệ thống ban đầu, nhưng có một bộ các kịch bản tái tạo đồ chơi. Họ bảo lãnh khi phát hiện trường hợp không hợp lệ. Tại sao nó được phát hiện? Tại sao nó lại xảy ra?

Câu hỏi thưởng

NASDAQ giải quyết vấn đề này như thế nào? Làm thế nào để cavirtex? Làm thế nào để mtgox?

tl; dr

Có ba khối kịch bản. Đặt chúng vào 3 tab SSMS riêng biệt và chạy chúng. Các tập lệnh thứ 2 và 3 sẽ đưa ra một lỗi. Giúp tôi tìm ra lý do tại sao họ xuất hiện lỗi.


Nó có lẽ là một cái gì đó để làm với mức độ cô lập giao dịch. Mức độ cô lập nào bạn đang sử dụng trong hệ thống của bạn?
cha

@cha Mặc định (ĐỌC CAM KẾT). Sao chép-dán các tập lệnh và bạn có thể xác nhận rằng nó thực sự là mức mặc định.
Ian Boyd

Khi tab thứ 3 của bạn "Đặt lại hàng bị lỗi", hàng đó sẽ khả dụng. Như vậy, tab thứ 2 của bạn có thể phân bổ nó trước khi tab thứ 3 đánh dấu hàng trước đó là có sẵn. Hãy thử thực hiện cả hai sửa đổi trong CẬP NHẬT trong tab thứ 3 của bạn.
AK

Câu trả lời:


12

Mức READ COMMITTEDcô lập giao dịch mặc định đảm bảo rằng giao dịch của bạn sẽ không đọc dữ liệu không được cam kết. Nó không đảm bảo rằng bất kỳ dữ liệu nào bạn đọc sẽ giữ nguyên nếu bạn đọc lại (đọc lặp lại) hoặc dữ liệu mới sẽ không xuất hiện (phantoms).

Những cân nhắc tương tự này áp dụng cho nhiều truy cập dữ liệu trong cùng một tuyên bố .

UPDATETuyên bố của bạn tạo ra một kế hoạch truy cập vào Transactionsbảng nhiều lần, do đó, nó dễ bị ảnh hưởng bởi các lần đọc và phantoms không lặp lại.

Nhiều truy cập

Có nhiều cách để kế hoạch này tạo ra kết quả mà bạn không mong đợi khi bị READ COMMITTEDcô lập.

Một ví dụ

TransactionsTruy cập bảng đầu tiên tìm thấy các hàng có trạng thái WaitingList. Truy cập thứ hai đếm số lượng mục (cho cùng một công việc) có trạng thái Booked. Truy cập đầu tiên chỉ có thể trả về giao dịch sau (giao dịch trước đó là Bookedtại thời điểm này). Khi truy cập (đếm) thứ hai xảy ra, giao dịch trước đó đã được thay đổi thành WaitingList. Hàng sau do đó đủ điều kiện để cập nhật Bookedtrạng thái.

Các giải pháp

Có một số cách để đặt ngữ nghĩa cách ly để có được kết quả mà bạn đang theo đuổi. Một tùy chọn là kích hoạt READ_COMMITTED_SNAPSHOTcơ sở dữ liệu. Điều này cung cấp tính nhất quán đọc mức câu lệnh cho các câu lệnh chạy ở mức cô lập mặc định. Không thể lặp lại đọc và phantoms theo cách ly chụp nhanh cam kết đọc.

Những chú ý khác

Tôi phải nói rằng mặc dù tôi sẽ không thiết kế lược đồ hoặc truy vấn theo cách này. Có nhiều công việc liên quan hơn là cần thiết để đáp ứng yêu cầu kinh doanh đã nêu. Có lẽ đây là một phần kết quả của sự đơn giản hóa trong câu hỏi, trong mọi trường hợp đó là một câu hỏi riêng biệt.

Hành vi bạn đang thấy không đại diện cho bất kỳ lỗi nào. Các kịch bản tạo ra kết quả chính xác cho ngữ nghĩa cách ly được yêu cầu. Hiệu ứng đồng thời như thế này cũng không giới hạn ở các gói truy cập dữ liệu nhiều lần.

Mức cô lập cam kết đọc cung cấp ít đảm bảo hơn nhiều so với thường được giả định. Ví dụ, bỏ qua các hàng và / hoặc đọc cùng một hàng nhiều lần là hoàn toàn có thể.


Tôi đang cố gắng tìm ra thứ tự của các hoạt động gây ra kết quả sai lầm. Nó lần đầu tiên INNERtham gia Transactionsđể Allocationsdựa trên WaitingListtrạng thái. Sự tham gia này xảy ra trước khi UPDATEmất bất kỳ IXhoặc Xkhóa. Bởi vì giao dịch đầu tiên vẫn còn Booked, INNER JOINchỉ tìm thấy giao dịch sau. Sau đó, nó truy cập vào Transactionsbảng một lần nữa để thực hiện LEFT OUTER JOINmột số lượng các vị trí có sẵn. Vào thời điểm này, giao dịch đầu tiên đã được cập nhật WaitingList, có nghĩa là có một vị trí.
Ian Boyd

Hệ thống thực có thêm mức độ phức tạp. Ví dụ: JobNamekhông (và không thể) được lưu trữ với Transactionnhưng với một Employee. Vì vậy, Transactionscó chứa một EmployeeID, và chúng ta phải tham gia. Phân bổ có sẵn cũng được xác định cho một ngày và một công việc . Vì vậy, Allocationsbảng thực sự là (TransactionDate, JobName). Cuối cùng, một người có thể có nhiều giao dịch trong cùng một ngày; mà chỉ phải chiếm 1 slot. Vì vậy, hệ thống thực sự làm distinct-countbởi Employee,Job,Date. Bỏ qua tất cả những điều đó, bạn sẽ thay đổi gì với đồ chơi? Có lẽ nó có thể được thông qua lại.
Ian Boyd

2
Re: bình luận đầu tiên, vâng (ngoại trừ đó không phải là một kết quả sai lầm). Re: bình luận thứ hai, đó sẽ là công việc tư vấn :)
Paul White 9

2
@AlexKuznetsov Dựa trên kiến ​​thức mới phát hiện của tôi, vấn đề kỳ nghỉ vé Arnie / Carol có thể xảy ra trong READ COMMITTEDsự cô lập. Đi kiểm tra kỳ nghỉ nếu có bất kỳ vé được giao cho tôi. Nếu kiểm tra của Ticketsbảng sử dụng một chỉ mục, nó sẽ nhầm tưởng rằng vé không được chỉ định cho tôi. Sau đó, ai đó chỉ định vé cho tôi và trình kích hoạt sử dụng một chỉ mục để nghĩ rằng tôi chưa đi nghỉ. Kết quả: một vé hoạt động được chỉ định cho một nhà phát triển vào kỳ nghỉ. Với kiến ​​thức mới này, tôi muốn nằm xuống và khóc; Toàn bộ thế giới của tôi bị hủy bỏ, mọi thứ tôi từng viết đều sai.
Ian Boyd

1
@IanBoyd đây là lý do tại sao chúng tôi sử dụng các ràng buộc để thực thi các quy tắc như quy tắc bạn gặp vấn đề. Chúng tôi đã thay thế trình kích hoạt cuối cùng bằng các ràng buộc hơn hai năm trước và chúng tôi đang tận hưởng tính toàn vẹn dữ liệu kín nước kể từ đó. Ngoài ra, chúng tôi không còn phải học về các khóa chi tiết tuyệt vời, các mức cô lập, v.v. - các ràng buộc chỉ hoạt động, miễn là bạn không sử dụng MERGE, tất nhiên.
AK
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.