Saya bukan penggemar besar dari tabel "kunci" tambahan atau gagasan mengunci seluruh meja untuk meraih rekor berikutnya. Saya mengerti mengapa hal itu dilakukan, tetapi itu juga mengganggu konkurensi untuk operasi yang memperbarui untuk merilis catatan yang terkunci (pasti dua proses tidak dapat memperdebatkan bahwa ketika dua proses tidak mungkin untuk mengunci catatan yang sama di waktu yang sama).
Preferensi saya adalah menambahkan kolom ProcessStatusID (biasanya TINYINT) ke tabel dengan data yang sedang diproses. Dan apakah ada bidang untuk LastModifiedDate? Jika tidak, maka harus ditambahkan. Jika ya, apakah catatan ini diperbarui di luar pemrosesan ini? Jika catatan dapat diperbarui di luar proses khusus ini, maka bidang lain harus ditambahkan untuk melacak StatusModifiedDate (atau sesuatu seperti itu). Untuk sisa jawaban ini saya hanya akan menggunakan "StatusModifiedDate" karena jelas artinya (dan pada kenyataannya, dapat digunakan sebagai nama bidang bahkan jika saat ini tidak ada bidang "LastModifiedDate").
Nilai untuk ProcessStatusID (yang harus ditempatkan ke tabel pencarian baru yang disebut "ProcessStatus" dan Foreign Keyed to table ini) bisa menjadi:
- Selesai (atau bahkan "Menunggu keputusan" dalam kasus ini karena keduanya berarti "siap untuk diproses")
- Dalam Proses (atau "Memproses")
- Kesalahan (atau "WTF?")
Pada titik ini tampaknya aman untuk mengasumsikan bahwa dari aplikasi, ia hanya ingin mengambil catatan selanjutnya untuk diproses dan tidak akan memberikan apa pun untuk membantu membuat keputusan itu. Jadi kami ingin mengambil catatan tertua (setidaknya dalam hal StatusModifiedDate) yang disetel ke "Selesai" / "Tertunda". Sesuatu di sepanjang garis:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
Kami juga ingin memperbarui catatan itu ke "Dalam Proses" pada saat yang sama untuk mencegah proses lainnya dari meraihnya. Kita dapat menggunakan OUTPUT
klausa untuk mengizinkan kami melakukan PEMBARUAN dan PILIH dalam transaksi yang sama:
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
Masalah utama di sini adalah bahwa sementara kita dapat melakukan TOP (1)
suatu UPDATE
operasi, tidak ada cara untuk melakukannya ORDER BY
. Tapi, kita bisa membungkusnya dalam CTE untuk menggabungkan dua konsep itu:
;WITH cte AS
(
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET cte.StatusID = 2,
cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;
Pertanyaan yang jelas adalah apakah dua proses melakukan SELECT pada saat yang sama dapat meraih catatan yang sama. Saya cukup yakin bahwa klausa UPDATE dengan OUTPUT, terutama dikombinasikan dengan petunjuk READPAST dan UPDLOCK (lihat di bawah untuk lebih jelasnya), akan baik-baik saja. Namun, saya belum menguji skenario yang tepat ini. Jika karena alasan tertentu permintaan di atas tidak mengurus kondisi balapan, maka menambahkan wasiat berikut: kunci aplikasi.
Kueri CTE di atas dapat dibungkus dengan sp_getapplock dan sp_releaseapplock untuk membuat "penjaga gerbang" untuk proses tersebut. Dengan demikian, hanya satu proses pada satu waktu akan dapat masuk untuk menjalankan kueri di atas. Proses lain akan diblokir sampai proses dengan applock melepaskannya. Dan karena langkah dari keseluruhan proses ini hanya untuk meraih RecordID, itu cukup cepat dan tidak akan memblokir proses lainnya terlalu lama. Dan, seperti halnya permintaan CTE, kami tidak memblokir seluruh tabel, sehingga memungkinkan pembaruan lain ke baris lain (untuk mengatur statusnya menjadi "Lengkap" atau "Kesalahan"). Pada dasarnya:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
Kunci aplikasi sangat bagus tetapi harus digunakan hemat.
Terakhir, Anda hanya perlu prosedur tersimpan untuk menangani pengaturan status baik "Selesai" atau "Kesalahan". Dan itu bisa sederhana:
CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
@RecordID INT,
@ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;
UPDATE pt
SET pt.ProcessStatusID = @ProcessStatusID,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM ProcessTable pt
WHERE pt.RecordID = @RecordID;
Petunjuk Meja (ditemukan di Petunjuk (Transact-SQL) - Tabel ):