我准备了一张发票表,然后准备打印。
[STATUS]
列是草稿,打印,打印,打印
我需要获取要打印的第一个(FIFO)记录的ID,并更改记录状态。该操作必须是线程安全的,以便其他进程不会选择相同的InvoiceID
我可以这样做(看起来很原始,但也许不是......):
WITH CTE AS
(
SELECT TOP(1) [InvoiceID], [Status]
FROM INVOICES
WHERE [Status] = 'Print'
ORDER BY [PrintRequestedDate], [InvoiceID]
)
UPDATE CTE
SET [Status] = 'Printing'
, @InvoiceID = [InvoiceID]
...使用@InvoiceID
...
UPDATE INVOICES
SET [Status] = 'Printed'
WHERE [InvoiceID] = @InvoiceID
或者我必须使用它(对于第一个声明)
UPDATE INVOICES
SET [Status] = 'Printing'
, @InvoiceID = [InvoiceID]
WHERE [InvoiceID] =
(
SELECT TOP 1 [InvoiceID]
FROM INVOICES WITH (UPDLOCK)
WHERE [Status] = 'Print'
ORDER BY [PrintRequestedDate], [InvoiceID]
)
...使用@InvoiceID
......等执行操作。
(我不能将交易状态从更改状态更改为"打印"直到流程结束,即状态最终更改为"打印")。
编辑:
如果重要,则数据库为READ_COMMITTED_SNAPSHOT
我可以为UPDATE STATUS和"打印"并获取ID。但我不能继续保持交易一直打开,将状态更改为" Printed"。这是一份SSRS报告,它对SQL进行了几次不同的查询以获取发票的各个部分,并且它可能会崩溃/无论如何都会使事务处于打开状态。
@Gordon Linoff"如果你想要一个队列" FIFO序列并不重要,我只想首先要求打印的发票......"或多或少" (不要有任何不必要的复杂性......)
@Martin Smith"看起来像通常的表作为队列要求" - 是的,正是如此,感谢非常有用的链接。
解决方案:
我采用的解决方案来自评论:
@ lad2025向我指出SQL Server Process Queue Race Condition使用WITH (ROWLOCK, READPAST, UPDLOCK)
和@MartinSmith解释了隔离问题是什么,并指出我Using tables as Queues - 它正在谈论我正在尝试做什么。
我没有理解为什么UPDATE TOP 1
是安全的,UPDATE MyTable SET xxx = yyy WHERE MyColumn = (SELECT TOP 1 SomeColumn FROM SomeTable ORDER BY AnotherColumn)
(没有隔离提示)不是,我应该教育自己,但我很高兴只是提出隔离提示在我的代码中继续使用其他东西:)
感谢您的帮助。
答案 0 :(得分:0)
我的担心是重复[InvoiceID]
同一[InvoiceID]
在第一次更新时,ONE行获取set [Status] = 'Printing'
在第二次更新时,所有[InvoiceID]行获得set [Status] = 'Printed'
这甚至可以设置status ='draft'
也许这就是你想要的
另一个进程可以在set [Status] = 'Print'
因此会打印一些副本,有些则不会
我评论使用update lock
这是不确定的,但您可以top (1)
并跳过order by
。您将倾向于获得最新的行,但不能保证。如果你清除队列,那么你就可以得到所有队列。
这表明你可能会失去'draft'= 1
declare @invID int;
declare @T table (iden int identity primary key, invID int, status tinyint);
insert into @T values (1, 2), (5, 1), (3, 1), (4, 1), (4, 2), (2, 1), (1, 1), (5, 2), (5, 2);
declare @iden int;
select * from @t order by iden;
declare @rowcount int = 1;
while (@ROWCOUNT > 0)
begin
update top (1) t
set t.status = 3, @invID = t.invID, @iden = t.iden
from @t t
where t.status = '2';
set @rowcount = @@ROWCOUNT;
if(@rowcount > 0)
begin
select @invID, @iden;
-- do stuff
update t
set t.status = 4
from @t t
where t.invID = @invID; -- t.iden = @iden;
select * from @T order by iden;
end
end
答案 1 :(得分:0)
我觉得你的代码很好。即因为你有一个语句,只要语句运行就将状态更新为printing
,状态就会更新;因此,在您的流程看到之前,搜索print
之前运行的任何内容都会将相同的记录更新为printing
;因此,您的流程会选择一个后续记录,或者在您的语句运行后遇到它的任何进程都会将其视为printing
,因此不会选择它。实际上并没有一个记录能够在语句运行时提取它的情况,因为单个SQL语句的讨论应该是原子的。
那就是说,我还不足以说出明确的锁定提示是否会有所帮助;在我看来他们不需要,因为上面是原子的,但评论中的其他人可能比我更了解情况。但是,运行测试(虽然数据库和两个线程都在同一台机器上运行)我无法创建竞争条件......也许如果客户端在不同的机器上/如果有更多的并发性你&# 39; d更有可能看到问题。
我的希望是其他人以不同的方式解释你的问题,因此存在分歧。
这是我曾经试图引起竞争条件的代码;您可以将其放入LINQPad 5
,选择语言C# Program
,根据需要调整连接字符串(以及可选的任何语句),然后运行:
const long NoOfRecordsToTest = 1000000;
const string ConnectionString = "Server=.;Database=Play;Trusted_Connection=True;"; //assumes a database called "play"
const string DropFifoQueueTable = @"
if object_id('FIFOQueue') is not null
drop table FIFOQueue";
const string CreateFifoQueueTable = @"
create table FIFOQueue
(
Id bigint not null identity (1,1) primary key clustered
, Processed bit default (0) --0=queued, null=processing, 1=processed
)";
const string GenerateDummyData = @"
with cte as
(
select 1 x
union all
select x + 1
from cte
where x < @NoRowsToGenerate
)
insert FIFOQueue(processed)
select 0
from cte
option (maxrecursion 0)
";
const string GetNextFromQueue = @"
with singleRecord as
(
select top (1) Id, Processed
from FIFOQueue --with(updlock, rowlock, readpast) --optionally include this per comment discussions
where processed = 0
order by Id
)
update singleRecord
set processed = null
output inserted.Id";
//we don't really need this last bit for our demo; I've included in case the discussion turns to this..
const string MarkRecordProcessed = @"
update FIFOQueue
set Processed = 1
where Id = @Id";
void Main()
{
SetupTestDatabase();
var task1 = Task<IList<long>>.Factory.StartNew(() => ExampleTaskForced(1));
var task2 = Task<IList<long>>.Factory.StartNew(() => ExampleTaskForced(2));
Task.WaitAll(task1, task2);
foreach (var processedByBothThreads in task1.Result.Intersect(task2.Result))
{
Console.WriteLine("Both threads processed id: {0}", processedByBothThreads);
}
Console.WriteLine("done");
}
static void SetupTestDatabase()
{
RunSql<int>(new SqlCommand(DropFifoQueueTable), cmd => cmd.ExecuteNonQuery());
RunSql<int>(new SqlCommand(CreateFifoQueueTable), cmd => cmd.ExecuteNonQuery());
var generateData = new SqlCommand(GenerateDummyData);
var param = generateData.Parameters.Add("@NoRowsToGenerate",SqlDbType.BigInt);
param.Value = NoOfRecordsToTest;
RunSql<int>(generateData, cmd => cmd.ExecuteNonQuery());
}
static IList<long> ExampleTaskForced(int threadId) => new List<long>(ExampleTask(threadId)); //needed to ensure prevent lazy loadling from causing issues with our tests
static IEnumerable<long> ExampleTask(int threadId)
{
long? x;
while ((x = ProcessNextInQueue(threadId)).HasValue)
{
yield return x.Value;
}
//yield return 55; //optionally return a fake result just to prove that were there a duplicate we'd catch it
}
static long? ProcessNextInQueue(int threadId)
{
var id = RunSql<long?>(new SqlCommand(GetNextFromQueue), cmd => (long?)cmd.ExecuteScalar());
//Debug.WriteLine("Thread {0} is processing id {1}", threadId, id?.ToString() ?? "[null]"); //if you want to see how we're doing uncomment this line (commented out to improve performance / increase the likelihood of a collision
/* then if we wanted to do the second bit we could include this
if(id.HasValue) {
var markProcessed = new SqlCommand(MarkRecordProcessed);
var param = markProcessed.Parameters.Add("@Id",SqlDbType.BigInt);
param.Value = id.Value;
RunSql<int>(markProcessed, cmd => cmd.ExecuteNonQuery());
}
*/
return id;
}
static T RunSql<T>(SqlCommand command, Func<SqlCommand,T> callback)
{
try
{
using (var connection = new SqlConnection(ConnectionString))
{
command.Connection = connection;
command.Connection.Open();
return (T)callback(command);
}
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
throw;
}
}
上面的讨论实际上只讨论了从队列中获取下一条记录的多个线程,同时避免了多个线程拾取任何单个记录。还有其他几点......
根据我们的讨论,如果FIFO是强制性的,还有其他事情需要担心。也就是说,虽然你的线程会按顺序拿起每条记录,然后由它们决定。例如Thread 1
获取记录10
,然后Thread 2
获得记录11
。现在Thread 2
在11
发送记录Thread 1
之前将记录10
发送到打印机。如果他们要去同一台打印机,那么您的打印件就会出现故障。如果他们不同的打印机,不是问题;任何打印机上的所有打印都是顺序的我将假设后者。
如果在处理某事的线程中发生任何异常(即线程的记录是printing
),那么应该考虑如何处理这个问题。一种选择是保持该线程重试;虽然这可能是不确定的,如果它是一些根本性的错误。另一种方法是将记录置于某个error
状态,以便由另一个进程处理/接受此记录不会按顺序出现。最后,如果队列中的发票顺序是理想的而不是硬性要求,您可以让拥有的线程将状态返回到print
,以便它或其他线程可以获取该记录以重试(尽管,如果记录存在根本性的错误,这可能会阻塞队列)。
我的建议是error
状态;因为那时你对这个问题有更多的了解/可以专门用另一个过程处理问题。
另一个问题是,由于您对printing
的更新未在交易中保留,如果服务器崩溃,您将在数据库中保留此状态的记录,并且当您的系统重新联机时忽略。避免这种情况的方法是包括一个列,说明哪个线程正在处理它;这样,当系统恢复时,该线程可以从中断处继续,或者包含一个日期戳,以便在一段时间后任何状态为printing
的记录超时&#34;可以根据需要扫描/重置为Error
或Print
状态。
WITH CTE AS
(
SELECT TOP(1) [InvoiceID], [Status], [ThreadId]
FROM INVOICES
WHERE [Status] = 'Print'
OR ([Status] = 'Printing' and [ThreadId] = @ThreadId) --handle previous crash
ORDER BY [PrintRequestedDate], [InvoiceID]
)
UPDATE CTE
SET [Status] = 'Printing'
, [ThreadId] = @ThreadId
OUTPUT Inserted.[InvoiceID]
我们主要专注于印刷元素;但其他进程也可能与您的Invoices
表进行交互。我们可以假设,除了创建初始Draft
记录并在准备好打印后将其更新为Print
之外,这些流程不会触及Status
字段。但是,完全不相关的进程可能会锁定相同的记录。如果我们想要确保FIFO,我们不能使用ReadPast
提示,因为某些记录可能具有状态Print
但是被锁定,所以我们跳过它们,尽管它们具有较早的{{1 }}。但是,如果我们想要尽可能快地打印出来的东西,并且在没有不方便的情况下将它们整理好,包括PrintRequestedDate
将允许我们的打印过程跳过锁定的记录并继续进行,一旦它们返回处理它们#39;重新发布。
同样,另一个进程可能会锁定我们在ReadPast
状态时的记录,因此我们无法对其进行更新以将其标记为已完成。同样,如果我们想要避免这种情况导致持久性,我们可以使用Printing
列来允许我们的线程将状态ThreadId
的记录保留下来,并在以后的时候返回清理它&# 39;没有锁定。显然,这假设Printing
列仅供我们的打印过程使用。
为了避免锁定发票的无关流程的某些问题,请将ThreadId
字段移到自己的表中;所以你只需要从Status
表中读取;不更新它。
这也有一个优点(如果您不关心打印历史记录),您可以在完成后删除记录,这样您就可以获得更好的性能(因为您不必搜索整个发票表以查找准备打印的那些表。也就是说,还有另一种选择(如果您使用SQL2008或更高版本)。
由于Status列会多次更新,因此它不是一个很好的索引候选者;即,随着状态的进展,索引中的记录位置从一个分支跳转到另一个分支。 但是,由于我们正在对其进行过滤,因此它也可以从索引中获益。 为了解决这个矛盾,可以选择使用过滤索引;即只索引我们对我们的印刷过程感兴趣的记录;因此我们维持一个小指数以获得巨大利益。
invoices
我怀疑你的示例使用字符串来保持演示代码的简单性,但是为了完整性而包括这一点。 在数据库中使用字符串会使事情难以支持。而不是使状态成为字符串值,而是使用相关状态表中的ID。
create nonclustered index ixf_Invoices_PrintStatusAndDate
on dbo.Invoices ([Status], [PrintRequestedDate])
include ([InvoiceId]) --just so we don't have to go off to the main table for this, but can get all we need form the index
where [Status] in ('Print','Printing')