Tôi không phải là một fan hâm mộ lớn của bảng "khóa" thêm hoặc ý tưởng khóa toàn bộ bảng để lấy bản ghi tiếp theo. Tôi hiểu lý do tại sao nó được thực hiện, nhưng điều đó cũng làm tổn hại đến sự đồng thời cho các hoạt động đang cập nhật để phát hành một bản ghi bị khóa (chắc chắn hai quy trình không thể chiến đấu với điều đó khi hai quy trình không thể khóa cùng một bản ghi tại cùng thời gian).
Sở thích của tôi sẽ là thêm một cột ProcessStatusID (thường là TINYINT) vào bảng với dữ liệu đang được xử lý. Và có một lĩnh vực cho LastModifiedDate? Nếu không, thì nó nên được thêm vào. Nếu có, thì những hồ sơ này có được cập nhật ngoài quá trình xử lý này không? Nếu các bản ghi có thể được cập nhật bên ngoài quy trình cụ thể này, thì nên thêm một trường khác để theo dõi StatusModifiedDate (hoặc một cái gì đó tương tự). Đối với phần còn lại của câu trả lời này, tôi sẽ chỉ sử dụng "StatusModifiedDate" vì nó rõ ràng theo nghĩa của nó (và trên thực tế, có thể được sử dụng làm tên trường ngay cả khi hiện tại không có trường "LastModifiedDate").
Các giá trị cho ProcessStatusID (cần được đặt vào bảng tra cứu mới có tên "ProcessStatus" và Foreign Keyed cho bảng này) có thể là:
- Đã hoàn thành (hoặc thậm chí "Đang chờ xử lý" trong trường hợp này vì cả hai đều có nghĩa là "sẵn sàng để được xử lý")
- Đang xử lý (hoặc "Đang xử lý")
- Lỗi (hoặc "WTF?")
Tại thời điểm này, có vẻ an toàn khi cho rằng từ ứng dụng, nó chỉ muốn lấy bản ghi tiếp theo để xử lý và sẽ không chuyển bất cứ điều gì để giúp đưa ra quyết định đó. Vì vậy, chúng tôi muốn lấy bản ghi cũ nhất (ít nhất là về StatusModifiedDate) được đặt thành "Đã hoàn thành" / "Đang chờ xử lý". Một cái gì đó dọc theo dòng:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
Chúng tôi cũng muốn cập nhật bản ghi đó thành "Đang xử lý" cùng một lúc để ngăn quá trình khác lấy nó. Chúng tôi có thể sử dụng OUTPUT
mệnh đề để cho phép chúng tôi thực hiện CẬP NHẬT và CHỌN trong cùng một giao dịch:
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
Vấn đề chính ở đây là trong khi chúng ta có thể làm một TOP (1)
trong một UPDATE
hoạt động, không có cách nào để làm một ORDER BY
. Nhưng, chúng ta có thể gói nó trong CTE để kết hợp hai khái niệm đó:
;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;
Câu hỏi rõ ràng là liệu hai quá trình thực hiện CHỌN cùng một lúc có thể lấy cùng một bản ghi hay không. Tôi khá chắc chắn rằng mệnh đề CẬP NHẬT với OUTPUT, đặc biệt là kết hợp với các gợi ý READPAST và UPDLOCK (xem bên dưới để biết thêm chi tiết), sẽ ổn. Tuy nhiên, tôi chưa thử nghiệm kịch bản chính xác này. Nếu vì một lý do nào đó, truy vấn trên không quan tâm đến điều kiện cuộc đua, thì thêm ý chí sau: khóa ứng dụng.
Truy vấn CTE ở trên có thể được gói trong sp_getapplock và sp_releaseapplock để tạo "trình giữ cổng" cho quy trình. Khi làm như vậy, chỉ có một quy trình tại một thời điểm sẽ có thể nhập để chạy truy vấn ở trên. Quá trình khác sẽ bị chặn cho đến khi quá trình với applock giải phóng nó. Và vì bước này của quy trình tổng thể chỉ là để lấy RecordID, nên nó khá nhanh và sẽ không chặn quá trình khác trong một thời gian dài. Và, giống như với truy vấn CTE, chúng tôi không chặn toàn bộ bảng, do đó cho phép các cập nhật khác cho các hàng khác (để đặt trạng thái của chúng thành "Đã hoàn thành" hoặc "Lỗi"). Bản chất:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
Khóa ứng dụng rất đẹp nhưng nên sử dụng một cách tiết kiệm.
Cuối cùng, bạn chỉ cần một thủ tục được lưu trữ để xử lý cài đặt trạng thái thành "Đã hoàn thành" hoặc "Lỗi". Và đó có thể là một điều đơn giản:
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;
Gợi ý bảng (được tìm thấy tại Gợi ý (Transact-SQL) - Bảng ):