使用交易来避免比赛

时间:2009-04-28 18:10:03

标签: sql ado.net transactions

我正在编写一个守护进程来监视新对象的创建,这会在检测到新内容时向数据库表添加行。我们将调用对象小部件。代码流大致如下:

1: every so often:
2:   find newest N widgets (from external source)
3:   foreach widget
4:     if( widget not yet in database )
5:       add rows for widget

最后两行显示竞争条件,因为如果此守护程序的两个实例同时运行,如果时间排列,它们可能都会为小部件X创建一行。

最明显的解决方案是在窗口小部件标识符列上使用unique约束,但由于数据库布局,这是不可能的(实际上允许窗口小部件有多行,但是守护进程不应该自动执行此操作。)

我的下一个想法是使用交易,因为这是他们的目的。在ADO.NET世界中,我相信我希望隔离级别为Serializable,但我并不积极。有人能指出我正确的方向吗?

更新:我做了一些实验,而Serialized事务似乎没有解决问题,或者至少不是很好。下面描述了有趣的情况,并假设只涉及一个表。请注意,我对锁定细节并不乐观,但我认为我说得对:

Thread A: Executes line 4, acquiring a read lock on the table
Thread B: Executes line 4, acquiring a read lock on the table
Thread A: Tries to execute line 5, which requires upgrading to a write lock
   (this requires waiting until Thread B unlocks the table)
Thread B: Tries to execute line 5, again requiring a lock upgrade
   (this requires waiting until Thread A unlocks)

这使我们处于典型的死锁状态。其他代码路径也是可能的,但如果线程A和B不交错,则无论如何都不存在同步问题。最终结果是在SQL检测到死锁并终止其中一个语句后,在其中一个线程上抛出SqlException。我可以捕获此异常并检测特定的错误代码,但这感觉不是很干净。

我可能采取的另一个方法是创建第二个表来跟踪守护程序看到的小部件,我可以使用unique约束。这仍然需要捕获和检测某些错误代码(在这种情况下,完整性约束违规),所以我仍然对更好的解决方案感兴趣,如果有人能想到的话。

4 个答案:

答案 0 :(得分:2)

通常,如果您同时使用数据库有多个进程或线程,则应始终使用事务。

隔离级别“可序列化”实际应该有效。它不允许从一个事务读取的数据被另一个事务更改。但它锁定了很多,一般不应该使用,因为它会降低应用程序的速度,并且存在更高的死锁风险。

备选方案:

  • 你只能锁定整个表,以确保在你检查某些东西是否存在时没有人写入。问题是,只有一个人可以同时写入表,这意味着这会减慢一切。 (您可以搜索数据,如果不存在,则在插入之前锁定表格并再次搜索。这是一种常见的模式。)
  • 老实说,你应该考虑两个事务试图同时插入同一行的事实。这可能是您应该开始解决它的地方。只有一个守护程序应该负责相同的数据。
    • 让每个守护进程处理自己的数据
    • 让每个守护进程调用服务而不是数据库。你可以在插入之前将它们放在一起。

顺便说一句,您需要一个唯一的数据标识符来清楚地识别它。如果您没有唯一标识符,如何在数据库中搜索它?

答案 1 :(得分:0)

如何检查'if(小部件尚未在数据库中)'。如果这是以“Select”的形式用sql编写的,则可以使用“Select for update”允许一次只能有一个守护进程实例执行此操作。通过使用最后两行的交易,并使用'select for update'来锁定,你将避免竞争。

我相信ado.net中有一个等价物。

答案 2 :(得分:0)

如果您有SQL Server 2008(或其他支持它的DBMS),请考虑使用MERGE语句。只有当行不存在时,才能写入以执行插入。

答案 3 :(得分:0)

  

(实际上允许有更多   小部件比一行多,但是   守护进程不应该这样做   自动地)

然后问题不是数据库。事实上,你有多个守护进程,不知道已经检测到同一个对象。在我看来,你需要集中注意力。从数据库的角度来看,他们都提交了完全合法的行。

如果更改会发生什么:

1: every so often:
2:   find newest N widgets (from external source)
3:   foreach widget
4:     if( widget not yet in database )
5:       add rows for widget

1: every so often:
2:   while there are unhandled widgets
2:       find first unhandled widget
4:       if( widget not yet in database )
5:           add one row for widget