Saya mencoba menjawab pertanyaan stackoverflow berikut:
Setelah mengirim jawaban yang agak naif, saya pikir saya akan meletakkan uang saya di tempat mulut saya dan benar-benar menguji skenario yang saya sarankan, untuk memastikan saya tidak mengirim OP dengan sia-sia. Ya, ternyata itu jauh lebih sulit dari yang saya kira (saya yakin tidak ada orang di sana).
Inilah yang saya coba dan pikirkan:
Pertama saya mencoba TOP 1 UPDATE dengan ORDER BY di dalam tabel turunan, menggunakan
ROWLOCK, READPAST
. Ini menghasilkan kebuntuan dan juga memproses barang-barang yang rusak. Itu harus sedekat mungkin dengan FIFO, kecuali kesalahan yang membutuhkan upaya untuk memproses baris yang sama lebih dari sekali.Saya kemudian mencoba memilih QueueID berikutnya yang diinginkan ke dalam variabel, dengan menggunakan berbagai kombinasi
READPAST
,UPDLOCK
,HOLDLOCK
, danROWLOCK
untuk secara eksklusif melestarikan baris untuk update dengan sesi tersebut. Semua variasi yang saya coba menderita dari masalah yang sama seperti sebelumnya serta, untuk kombinasi tertentu denganREADPAST
, mengeluh:Anda hanya dapat menentukan kunci READPAST di tingkat isolasi READ COMMITTED atau REPEATABLE READ.
Hal ini membingungkan karena itu BACA BERTEKAD. Saya pernah mengalami ini sebelumnya dan itu membuat frustrasi.
Sejak saya mulai menulis pertanyaan ini, Remus Rusani memposting jawaban baru untuk pertanyaan itu. Saya membaca artikel tertautnya dan melihat bahwa dia menggunakan bacaan yang merusak, karena dia mengatakan dalam jawabannya bahwa "tidak mungkin secara realistis untuk menahan kunci selama durasi panggilan web." Setelah membaca apa yang dikatakan artikelnya tentang hot spot dan halaman yang membutuhkan penguncian untuk melakukan pembaruan atau menghapus, saya khawatir bahwa bahkan jika saya dapat menemukan kunci yang benar untuk melakukan apa yang saya cari, itu tidak akan dapat diskalakan dan dapat tidak menangani konkurensi besar-besaran.
Saat ini saya tidak yakin ke mana harus pergi. Benarkah mempertahankan kunci saat baris diproses tidak dapat dicapai (bahkan jika itu tidak mendukung tps tinggi atau concurrency besar-besaran)? Apa yang saya lewatkan?
Dengan harapan orang-orang yang lebih pintar daripada saya dan orang-orang yang lebih berpengalaman daripada saya dapat membantu, di bawah ini adalah naskah tes yang saya gunakan. Itu kembali ke metode TOP 1 UPDATE tapi saya meninggalkan metode lain, berkomentar, kalau-kalau Anda ingin menjelajahi itu juga.
Rekatkan masing-masing ke dalam sesi terpisah, jalankan sesi 1, lalu cepat-cepat yang lainnya. Dalam sekitar 50 detik tes akan berakhir. Lihat Pesan dari setiap sesi untuk melihat pekerjaan apa yang dilakukan (atau bagaimana gagal). Sesi pertama akan menampilkan rowset dengan snapshot diambil satu detik sekali yang merinci hadirnya kunci dan item antrian yang sedang diproses. Kadang-kadang itu berfungsi, dan di waktu lain tidak bekerja sama sekali.
Sesi 1
/* Session 1: Setup and control - Run this session first, then immediately run all other sessions */
IF Object_ID('dbo.Queue', 'U') IS NULL
CREATE TABLE dbo.Queue (
QueueID int identity(1,1) NOT NULL,
StatusID int NOT NULL,
QueuedDate datetime CONSTRAINT DF_Queue_QueuedDate DEFAULT (GetDate()),
CONSTRAINT PK_Queue PRIMARY KEY CLUSTERED (QueuedDate, QueueID)
);
IF Object_ID('dbo.QueueHistory', 'U') IS NULL
CREATE TABLE dbo.QueueHistory (
HistoryDate datetime NOT NULL,
QueueID int NOT NULL
);
IF Object_ID('dbo.LockHistory', 'U') IS NULL
CREATE TABLE dbo.LockHistory (
HistoryDate datetime NOT NULL,
ResourceType varchar(100),
RequestMode varchar(100),
RequestStatus varchar(100),
ResourceDescription varchar(200),
ResourceAssociatedEntityID varchar(200)
);
IF Object_ID('dbo.StartTime', 'U') IS NULL
CREATE TABLE dbo.StartTime (
StartTime datetime NOT NULL
);
SET NOCOUNT ON;
IF (SELECT Count(*) FROM dbo.Queue) < 10000 BEGIN
TRUNCATE TABLE dbo.Queue;
WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
B (N) AS (SELECT 1 FROM A Z, A I, A P),
C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B O, B W)
INSERT dbo.Queue (StatusID, QueuedDate)
SELECT 1, DateAdd(millisecond, C.N * 3, GetDate() - '00:05:00')
FROM C
WHERE C.N <= 10000;
END;
TRUNCATE TABLE dbo.StartTime;
INSERT dbo.StartTime SELECT GetDate() + '00:00:15'; -- or however long it takes you to go run the other sessions
GO
TRUNCATE TABLE dbo.QueueHistory;
SET NOCOUNT ON;
DECLARE
@Time varchar(8),
@Now datetime;
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 33 BEGIN
SET @Now = GetDate();
INSERT dbo.QueueHistory
SELECT
@Now,
QueueID
FROM
dbo.Queue Q WITH (NOLOCK)
WHERE
Q.StatusID <> 1;
INSERT dbo.LockHistory
SELECT
@Now,
L.resource_type,
L.request_mode,
L.request_status,
L.resource_description,
L.resource_associated_entity_id
FROM
sys.dm_tran_current_transaction T
INNER JOIN sys.dm_tran_locks L
ON L.request_owner_id = T.transaction_id;
WAITFOR DELAY '00:00:01';
SET @i = @i + 1;
END;
WITH Cols AS (
SELECT *, Row_Number() OVER (PARTITION BY HistoryDate ORDER BY QueueID) Col
FROM dbo.QueueHistory
), P AS (
SELECT *
FROM
Cols
PIVOT (Max(QueueID) FOR Col IN ([1], [2], [3], [4], [5], [6], [7], [8])) P
)
SELECT L.*, P.[1], P.[2], P.[3], P.[4], P.[5], P.[6], P.[7], P.[8]
FROM
dbo.LockHistory L
FULL JOIN P
ON L.HistoryDate = P.HistoryDate
/* Clean up afterward
DROP TABLE dbo.StartTime;
DROP TABLE dbo.LockHistory;
DROP TABLE dbo.QueueHistory;
DROP TABLE dbo.Queue;
*/
Sesi 2
/* Session 2: Simulate an application instance holding a row locked for a long period, and eventually abandoning it. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE
@QueueID int,
@Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime + '0:00:01', 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
----OUTPUT Inserted.*
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
WAITFOR DELAY '00:00:20'; -- Release it partway through the test
ROLLBACK TRAN; -- Simulate client disconnecting
Sesi 3
/* Session 3: Run a near-continuous series of "failed" queue processing. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE
@QueueID int,
@EndDate datetime,
@NextDate datetime,
@Time varchar(8);
SELECT
@EndDate = StartTime + '0:00:33',
@Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
WHILE GetDate() < @EndDate BEGIN
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
----OUTPUT Inserted.*
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
SET @NextDate = GetDate() + '00:00:00.015';
WHILE GetDate() < @NextDate SET NOCOUNT ON;
ROLLBACK TRAN;
END
Sesi 4 dan lebih tinggi - sebanyak yang Anda suka
/* Session 4: "Process" the queue normally, one every second for 30 seconds. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE @Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 30 BEGIN
BEGIN TRAN;
--SET @QueueID = (
-- SELECT TOP 1 QueueID
-- FROM dbo.Queue WITH (READPAST, UPDLOCK)
-- WHERE StatusID = 1 -- ready
-- ORDER BY QueuedDate, QueueID
--);
--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
--WHERE QueueID = @QueueID;
SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
SELECT TOP 1 *
FROM dbo.Queue WITH (ROWLOCK, READPAST)
WHERE StatusID = 1
ORDER BY QueuedDate, QueueID
) Q
PRINT @QueueID;
WAITFOR DELAY '00:00:01'
SET @i = @i + 1;
DELETE dbo.Queue
WHERE QueueID = @QueueID;
COMMIT TRAN;
END
READPAST, UPDLOCK, ROWLOCK
skrip saya untuk mengambil data ke tabel QueueHistory tidak melakukan apa-apa. Saya ingin tahu apakah itu karena StatusID tidak berkomitmen? Itu menggunakan WITH (NOLOCK)
sehingga secara teoritis harus bekerja ... dan itu berhasil sebelum! Saya tidak yakin mengapa itu tidak berfungsi sekarang, tapi mungkin itu pengalaman belajar yang lain.