我正在编写一个后台服务,该服务需要处理一系列作业,这些作业作为记录存储在sqlserver表中。服务需要找到需要处理的最旧的20个作业(where status = 'new'),标记它们(set status = 'processing'),运行它们,然后再更新这些作业。
where status = 'new'
set status = 'processing'
这是我需要帮助的第一部分。可能有多个线程同时访问数据库,并且我想确保“标记并返回”查询是原子运行的,或者几乎是这样运行的。
这项服务将花费相对较少的时间访问数据库,并且如果一个作业运行两次,这不是世界末日,因此,为了提高代码的简单性,我可能能够接受一次运行多次的可能性很小。
做这个的最好方式是什么?我在数据层上使用linq-to-sql,但是我想为此必须使用t-sql。
您的工作表是一个队列。众所周知,编写用户表备份队列很容易出错,因为这会导致死锁和并发问题。
最简单的事情是删除用户表并使用真实队列。这将使您在经过系统测试和验证的代码库上获得无死锁且无并发的队列。问题在于,围绕队列的整个范例从INSERT和DELETE / UPDATE更改为SEND / RECEIVE。另一方面,使用内置队列,您可以获得一些非常强大的免费功能,即激活和相关项锁定。
如果要继续沿用户表支持队列的路径前进,那么编写用户表队列的 第二个 最重要的技巧是使用UPDATE … OUTPUT:
WITH cte AS ( SELECT TOP(20) status, id, ... FROM table WITH (ROWLOCK, READPAST, UPDLOCK) WHERE status = 'new' ORDER BY enqueue_time) UPDATE cte SET status = 'processing' OUTPUT INSERTED.id, ...
CTE语法只是为了方便正确放置TOP和ORDER BY,可以使用派生表同样方便地编写查询。您不能使用直接UPDATE … TOP,因为UPDATE不支持ORDER BY,并且您需要使用它来满足需求中“最旧的”部分。需要使用锁提示来促进并行处理线程之间的高度并发性。
我说这是第二个最重要的把戏。最重要的是如何组织表格。对于队列,它 必须 由聚类(status, enqueue_time)。如果您没有正确地组织表格,那么最终将导致死锁。先发制人的评论:在这种情况下碎片是无关紧要的。
(status, enqueue_time)