Kami memiliki masalah, selama konkurensi tinggi, kueri yang mengembalikan hasil yang tidak masuk akal - mengakibatkan pelanggaran logika kueri yang dikeluarkan. Butuh beberapa saat untuk mereproduksi masalah ini. Saya telah berhasil menyaring masalah yang dapat direproduksi ke beberapa genggam T-SQL.
Catatan : Bagian dari sistem live yang memiliki masalah ini terdiri dari 5 tabel, 4 pemicu, 2 prosedur tersimpan, dan 2 tampilan. Saya telah menyederhanakan sistem nyata menjadi sesuatu yang jauh lebih mudah dikelola untuk pertanyaan yang diposting. Hal-hal telah dikupas, kolom dihapus, prosedur tersimpan dibuat sejajar, tampilan berubah menjadi ekspresi tabel umum, nilai kolom berubah. Ini semua jauh untuk mengatakan bahwa sementara yang berikut mereproduksi kesalahan, mungkin lebih sulit untuk dipahami. Anda harus berhenti bertanya-tanya mengapa ada sesuatu yang terstruktur seperti itu. Saya di sini mencoba mencari tahu mengapa kondisi kesalahan terjadi berulang dalam model mainan ini.
/*
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
Transaksi keduanya dimasukkan sebagai WaitingList
. Selanjutnya kita memiliki tugas periodik yang berjalan, mencari slot kosong dan menabrak siapa pun di WaitingList menjadi status Tercatat.
Di jendela SSMS yang terpisah, kami memiliki prosedur tersimpan berulang yang disimulasikan:
/*
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
Dan akhirnya jalankan ini di jendela koneksi SSMS ke-3. Ini mensimulasikan masalah konkurensi di mana transaksi sebelumnya berubah dari mengambil slot, menjadi berada di daftar tunggu:
/*
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
Secara konseptual, prosedur menabrak terus mencari slot kosong. Jika menemukannya, diperlukan transaksi paling awal yang ada di WaitingList
dan tandai sebagai Booked
.
Ketika diuji tanpa konkurensi, logikanya berfungsi. Kami memiliki dua transaksi:
- 12:00 siang: Daftar Tunggu
- 12.20 siang: Daftar Tunggu
Ada 1 alokasi, dan 0 transaksi dipesan, jadi kami menandai transaksi sebelumnya sebagai dipesan:
- 12:00 siang: Dipesan
- 12.20 siang: Daftar Tunggu
Lain kali tugas berjalan, sekarang ada 1 slot yang diambil - jadi tidak ada yang diperbarui.
Jika kami kemudian memperbarui transaksi pertama, dan memasukkannya ke WaitingList
:
UPDATE Transactions SET ApprovalStatus='WaitingList'
WHERE TransactionID = 60981
Lalu kita kembali ke tempat kita mulai:
- 12:00 siang: Daftar Tunggu
- 12.20 siang: Daftar Tunggu
Catatan : Anda mungkin bertanya-tanya mengapa saya mengembalikan transaksi ke daftar tunggu. Itu adalah korban dari model mainan yang disederhanakan. Dalam sistem nyata transaksi bisa
PendingApproval
, yang juga menempati slot. Transaksi PendingApproval dimasukkan ke dalam daftar tunggu ketika disetujui. Tidak masalah. Jangan khawatir tentang itu.
Tetapi ketika saya memperkenalkan konkurensi, dengan memiliki jendela kedua yang secara konstan menempatkan transaksi pertama kembali ke daftar tunggu setelah dipesan, maka transaksi selanjutnya berhasil mendapatkan pemesanan:
- 12:00 siang: Daftar Tunggu
- 12.20 siang: Dipesan
Skrip tes mainan menangkap ini, dan berhenti mengulangi:
Msg 50000, Level 16, State 1, Line 41
The later tranasction, that should never be booked, managed to get booked!
Mengapa?
Pertanyaannya adalah, mengapa dalam model mainan ini, apakah kondisi bail-out ini dipicu?
Ada dua kemungkinan status untuk status persetujuan transaksi pertama:
- Memesan : dalam hal ini slot ditempati, dan transaksi selanjutnya tidak dapat memilikinya
- WaitingList : dalam hal ini ada satu slot kosong, dan dua transaksi yang menginginkannya. Tapi karena kita selalu
select
yang tertua transaksi (yaituORDER BY CreatedDate
) transaksi pertama harus mendapatkannya.
Saya pikir mungkin karena indeks lain
Saya mengetahui bahwa setelah UPDATE dimulai, dan data telah dimodifikasi, dimungkinkan untuk membaca nilai-nilai lama. Dalam kondisi awal:
- Indeks berkerumun :
Booked
- Indeks non-cluster :
Booked
Kemudian saya melakukan pembaruan, dan sementara simpul daun indeks berkerumun telah dimodifikasi, setiap indeks yang tidak berkerumun masih mengandung nilai asli dan masih tersedia untuk dibaca:
- Indeks berkerumun (kunci eksklusif):
Booked
WaitingList
- Indeks tidak berkerumun : (tidak dikunci)
Booked
Tapi itu tidak menjelaskan masalah yang diamati. Ya transaksi tidak lagi Dipesan , artinya sekarang ada slot kosong. Tetapi perubahan itu belum dilakukan, masih dilakukan secara eksklusif. Jika prosedur menabrak berjalan, itu akan menjadi:
- blok: jika opsi database isolasi snapshot tidak aktif
- baca nilai lama (mis.
Booked
): jika isolasi snapshot aktif
Either way, pekerjaan menabrak tidak akan tahu ada slot kosong.
Jadi saya tidak tahu
Kami telah berjuang selama berhari-hari untuk mencari tahu bagaimana hasil yang tidak masuk akal ini bisa terjadi.
Anda mungkin tidak mengerti sistem aslinya, tetapi ada satu set skrip yang dapat direproduksi mainan. Mereka menyelamatkan ketika kasus yang tidak valid terdeteksi. Mengapa terdeteksi? Mengapa ini terjadi?
Pertanyaan Bonus
Bagaimana NASDAQ mengatasi ini? Bagaimana cara cavirtex? Bagaimana mtgox?
tl; dr
Ada tiga blok skrip. Masukkan mereka ke dalam 3 tab SSMS yang terpisah dan jalankan. Skrip ke-2 dan ke-3 akan memunculkan kesalahan. Bantu saya mencari tahu mengapa kesalahan itu muncul.