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 WaitingList
và đá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
select
là 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.