Strategi untuk "memeriksa" catatan untuk diproses


10

Saya tidak yakin apakah ada pola nama untuk ini, atau jika tidak ada karena itu adalah ide yang buruk. Tetapi saya membutuhkan layanan saya untuk beroperasi di lingkungan yang seimbang muatan aktif / aktif. Ini adalah server aplikasi saja. Basis data akan berada di server terpisah. Saya memiliki layanan yang perlu dijalankan melalui proses untuk setiap catatan dalam sebuah tabel. Proses ini dapat memakan waktu satu atau dua menit, dan akan diulang setiap n menit (dapat dikonfigurasi, biasanya 15 menit).

Dengan tabel 1000 catatan yang membutuhkan pemrosesan ini, dan dua layanan yang berjalan terhadap kumpulan data yang sama ini, saya ingin setiap layanan "memeriksa" catatan untuk diproses. Saya perlu memastikan bahwa hanya satu layanan / utas yang memproses setiap catatan sekaligus.

Saya memiliki kolega yang telah menggunakan "meja kunci" di masa lalu. Di mana catatan ditulis ke tabel ini untuk secara logis mengunci catatan di tabel lain (bahwa tabel lain cukup statis, dan dengan catatan baru yang sangat sesekali ditambahkan), dan kemudian dihapus untuk melepaskan kunci.

Saya bertanya-tanya apakah itu tidak akan lebih baik untuk tabel baru memiliki kolom yang menunjukkan kapan itu dikunci, dan bahwa itu saat ini dikunci, alih-alih menyisipkan penghapusan secara terus-menerus.

Adakah yang punya tip untuk hal semacam ini? Apakah ada pola yang ditetapkan untuk penguncian logis jangka panjang (ish)? Adakah tips untuk memastikan hanya satu layanan yang dapat mengambil kunci sekaligus? (Rekan saya menggunakan TABLOCKX untuk mengunci seluruh tabel.)

Jawaban:


12

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:

  1. Selesai (atau bahkan "Menunggu keputusan" dalam kasus ini karena keduanya berarti "siap untuk diproses")
  2. Dalam Proses (atau "Memproses")
  3. 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 OUTPUTklausa 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 UPDATEoperasi, 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 ):

  • READPAST (sepertinya cocok dengan skenario ini)

    Menentukan bahwa Mesin Database tidak membaca baris yang dikunci oleh transaksi lainnya. Ketika READPAST ditentukan, kunci level baris dilewati. Yaitu, Mesin Basis Data melompat melewati baris alih-alih memblokir transaksi saat ini sampai kunci dilepaskan ... READPAST terutama digunakan untuk mengurangi pertikaian penguncian ketika menerapkan antrian kerja yang menggunakan tabel SQL Server. Pembaca antrian yang menggunakan READPAST melompati entri antrian yang dikunci oleh transaksi lain ke entri antrian yang tersedia berikutnya, tanpa harus menunggu sampai transaksi lain melepaskan kunci mereka.

  • ROWLOCK (hanya untuk aman)

    Menentukan bahwa kunci baris diambil ketika kunci halaman atau tabel biasanya diambil.

  • UPDLOCK

    Menentukan bahwa kunci pembaruan harus diambil dan ditahan hingga transaksi selesai. UPDLOCK mengambil kunci pembaruan untuk operasi hanya baca di tingkat baris atau tingkat halaman.


1

Melakukan hal serupa (tanpa aplikasi, murni dalam DB) menggunakan antrian Broker Layanan. Ringan, sepenuhnya sesuai ACID, dapat ditingkatkan hampir tak terhingga. Penguncian baris transparan (atau "bersembunyi", lebih tepatnya) sudah ada di dalamnya. Tersedia mulai versi 2005 dan seterusnya.

Dalam kasus Anda, arsitektur keseluruhan bisa seperti ini: beberapa proses mengirim pesan ke dialog Pialang Layanan, sesuai jadwal mereka, dan pendengar mengambilnya dari antrian di sisi target. Selain membuat jenis pesan yang terpisah, Anda dapat memasukkan hampir semua hal ke dalam badan pesan - batas waktu, misalnya, dan parameter apa pun yang mungkin dimiliki tugas tersebut.

Bukan hal yang paling mudah untuk dipahami, itu sudah pasti, tetapi begitu Anda mendapatkannya, keuntungannya akan menjadi jelas.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.