【问题标题】:How to prevent duplicate downloads from SQL database by multiple clients?如何防止多个客户端从 SQL 数据库重复下载?
【发布时间】:2015-07-31 09:47:00
【问题描述】:

我正在使用 C# 开发一个应用程序,其中一部分将从我的 SQL 数据库下载数据,然后发送到打印机。打印完成后,它会将数据标记为已打印,因此我不会多次打印相同的数据。

它在一台计算机/打印机上运行良好,但如果我让这个应用程序在多台客户端计算机上运行怎么办?好像一个客户端下载完数据后,在数据被标记为完成之前,另一个客户端会认为它没有下载并下载相同的数据。

有没有办法防止这种情况发生?我在想可能在下载数据时标记数据或确保只能同时执行 1 个查询?

【问题讨论】:

  • 将其标记为“进行中”,而不仅仅是布尔值。
  • 还用打印操作开始时间的时间戳标记它,这样没有人会等待一周的进行中的打印作业死亡。

标签: c# sql duplicates printers


【解决方案1】:

添加某种形式的“PrintStatus”列。

  • 添加数据时,它被设置为(比如)1,“未打印”。
  • 当客户端查询要打印的数据时,仅返回 PrintStatus = 1 的数据
  • 当客户端获取要打印的数据时,将其设置为(比如说)2,“正在打印”
  • 当客户端完成打印后,将状态设置为 3,“已打印”
  • 如果由于某种原因客户端无法打印数据,请将状态设置回 1(并将 PrintedAt 设置回 null)

这留下了看门人问题,也就是如果打印失败并且没有报告该事实会发生什么(因为看门人拔掉了服务器上的插头。别笑,它发生在这里,虽然它是一个技术维修人员,他真的应该更清楚。)如果您担心这些事情:

  • 添加某种形式的时间记录,例如列“PrintedAt”,当 PrintStatus 设置为 1 时将其设置为 null,并在 PrintStatus 设置为 2 时设置为 getdate()。附带好处:现在您知道何时(或也许是“哪个打印”)您的数据被打印了!
  • 打印完成后,PrintStatus 设置为 3。因此,如果 PrintStatus = 2,则打印(到目前为止)需要 getdate() – PrintedAt 运行时间。
  • 定期运行定期安排的进程(SQL 代理作业对此很有用)。它检查表,如果找到 PrintStatus = 2 的任何行,其中 PrintedAt 列大于您的容差(1 天?2 小时?)。通过将 PrintStatus 设置为 1 并将 PrintedAt 设置为 null,任何此类行都会“回滚”。

这看起来有点矫枉过正,但你告诉我一致数据对你来说有多重要。


添加:

是的,并发性和一致性是个问题。虽然您可以使用BEGIN TRANSACTION ... COMMIT/ROLLBACK,但我更喜欢利用隐式事务的力量。这是一种方法:

这设置了一个包含一些数据的测试表:

--  Set up test data
CREATE TABLE Test
 (
   Data         int       not null identity(1,1)
  ,PrintStatus  tinyint   not null
  ,PrintedAt    datetime  null
 )

INSERT Test (PrintStatus) values
 (1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)

SELECT *
 from Test
GO

该位包含更新所有要打印的行的 UPDATE 语句,并使用 OUTPUT 子句将这些项目存储到临时表中。然后将该表的内容返回到打印例程。

--  First pass: Get everything to pring
DECLARE @ToBePrinted table
 (Data  int  not null)


UPDATE Test
 set
   PrintStatus = 2
  ,PrintedAt = getdate()
 output inserted.Data into @ToBePrinted (Data)
 where PrintStatus = 1


SELECT Data PrintThese
 from @ToBePrinted


SELECT * from Test

GO

在这里,我们再添加三行数据,然后再次运行例程。旧项目未被选中,新项目被选中。

--  Second pass: Add three new items, they (and only they) get selected
INSERT Test (PrintStatus) values
 (1),(1),(1)

SELECT *
 from Test


DECLARE @ToBePrinted table
 (Data  int  not null)


UPDATE Test
 set
   PrintStatus = 2
  ,PrintedAt = getdate()
 output inserted.Data into @ToBePrinted (Data)
 where PrintStatus = 1


SELECT Data PrintThese
 from @ToBePrinted


SELECT * from Test

这是因为 SQL Server 的 ACID 属性——特别是 I)solation。一旦一条语句修改了表的内容,在该语句执行完成之前,其他语句都不能访问正在修改的数据。 (除非你傻到使用 NOLOCK。不要使用 NOLOCK。)

【讨论】:

  • 您应该添加针对并​​发请求的保护。在这种情况下,该解决方案没有具体说明如何确保只有一个进程可以“拾取要打印的数据”。如前所述,其中存在并发弱点。
  • 感谢您的回复!我现在担心的是,如果说客户端 A 抓取“未打印”行,并且在客户端 A 将该行标记为“正在打印”之前,客户端 B 开始并抓取同一行怎么办? SQL中的一种方法是在选择时标记该行吗?我正在阅读一些关于“选择...更新”的文档。但这似乎并不能阻止“选择”
  • 我添加了一种方法来执行此操作,并附有简要说明。要全面了解,请搜索“数据库酸”并阅读有关该主题的大量文章。 (这样做非常值得您花时间!)
  • (1 个月后)自我注意:在向新 SO 成员提供长而详细的答案之前要三思而后行
【解决方案2】:

我认为你在正确的轨道上。将布尔/位 IsComplete 字段更改为状态字段(使用“New”、“In Progress”和“Complete”等值)或添加一个名为 IsInProgress 的新位字段。然后只下载尚未进行中或完成的那些。

如果有某种方式下载或打印可能会失败,请确保您有逻辑将该行设置回“新”状态,以便稍后将其拾取并重试。或者,您可能希望将其标记为不同的错误状态,而不是作为异常过程的一部分进行处理。

【讨论】:

  • 两个并发连接竞相获得同一行怎么样?这需要考虑到并发性来加强。
  • 嗨匹兹堡 DBA,我想你说的是我现在最关心的问题,你会怎么做才能防止赛车连接出现在同一行?
  • 我建议您这样做:通过使用带有 guid/uniqueid 的新字段(可能是 BatchGUID)并使用 WITH (ROWLOCK) 来更新您要处理的行,这样就没有其他进程可以读取当你更新它时。然后,使用相同的 GUID 读取这些行并处理它们。
猜你喜欢
  • 1970-01-01
  • 2015-10-08
  • 1970-01-01
  • 2010-10-29
  • 2019-01-31
  • 1970-01-01
  • 1970-01-01
  • 2013-10-07
  • 2018-05-23
相关资源
最近更新 更多