IsolationLevel.RepeatableRead 防止重复

Posted

技术标签:

【中文标题】IsolationLevel.RepeatableRead 防止重复【英文标题】:IsolationLevel.RepeatableRead to prevent duplicates 【发布时间】:2010-09-12 13:33:24 【问题描述】:

我正在开发一个应用程序,该应用程序应该在收到 PayPal 即时付款通知时创建产品(例如运输保险单)。不幸的是,PayPal 有时会发送重复的通知。此外,还有另一个第三方在从 PayPal 获取更新时也会同时执行 Web 服务更新。

这是所涉及的数据库表的基本图。

// table "package"
// columns packageID, policyID, other data...
// 
// table "insurancepolicy"
// columns policyID, coverageAmount, other data...

这是我想要做的基本图表:

using (SqlConnection conn = new SqlConnection(...))

  sqlTransaction sqlTrans = conn.BeginTransaction(IsolationLevel.RepeatableRead);

  // Calls a stored procedure that checks if the foreign key in the transaction table has a value.
  if (PackageDB.HasInsurancePolicy(packageID, conn))
   
    sqlTrans.Commit();
    return false;
  

  // Insert row in foreign table.
  int policyID = InsurancePolicyDB.Insert(coverageAmount, conn);
  if (policyID <= 0)
  
    sqlTrans.Rollback();
    return false;
  

  // Assign foreign key to parent table.  If this fails, roll back everything.
  bool assigned = PackageDB.AssignPolicyID(packageID, policyID, conn);
  if (!assigned)
  
    sqlTrans.Rollback();
    return false;
  

如果有两个(或更多)线程(或进程或应用程序)同时执行此操作,我希望第一个线程在没有 policyID 时锁定“包”行,直到创建策略并且policyID 分配给包表。然后在将 policyID 分配给包表后,将释放锁。我希望调用相同代码的另一个线程在读取包行时会暂停,以确保它首先没有 policyID。当第一个事务的锁被释放时,我希望第二个事务会看到 policyID 在那里,因此返回而不向策略表中插入任何行。

注意:由于 CRUD 数据库设计,每个存储过程都涉及读取(选择)、创建(插入)或更新。

这是对RepeatableRead事务隔离的正确使用吗?

谢谢。

【问题讨论】:

什么是包:策略关系?是1:1吗? 它是 n:1。父表实际上不是用于“包”的,我只是为了说明而称它们为。实际上,这些“包裹”就像可以组合在一起运输的订单,为整个包裹提供一份保险单。 【参考方案1】:

我同意 aaronjensen 回复中的“消息队列”想法。如果您担心多个并发线程试图同时更新同一行数据,则应该让线程将其数据插入工作队列,然后由单个线程按顺序处理。这显着减少了数据库争用,因为目标表仅由一个线程而不是“N”个线程更新,并且工作队列操作仅限于消息线程的插入和数据处理线程的读取/更新。

【讨论】:

【参考方案2】:

如果insert into Policy 只是在尝试插入重复项时遇到一些唯一性表约束,那会更安全、更干净。提高隔离级别会降低并发性并导致死锁等其他令人讨厌的问题。

另一种方法是始终插入 Policy 行,如果 Package 已附加到 Policy,则将其回滚:

begin tran (read committed)

/* tentatively insert new Policy */
insert Policy

/* attach Package to Policy if it's still free */
update Package
  set Package.policy_id = @policy_id
  where Package.package_id = @package_id and Package.policy_id is null

if @@rowcount > 0
  commit
else
  rollback

这在冲突很少发生时效果最好,这似乎是你的情况。

【讨论】:

查看我上面关于 n:1 关系的评论,这使我无法将 packageID 字段放入 InsurancePolicy 表中。谢谢! 谢谢!我不知道为什么我没有想到“and Package.policy_id is null”部分!【参考方案3】:

我相信您实际上想要 Serializable 隔离级别。问题是两个线程可以通过 HasInsurancePolicyCheck(尽管我不知道 InsurancePolicyDB.Insert 会做什么或为什么会返回 0)

您还有许多其他选择。一种是使用消息队列并自己串行处理这些请求。另一种方法是使用sp_getapplock 并锁定该包独有的一些密钥。这样您就不会锁定更多的行或表。

【讨论】:

谢谢!公司里的其他人也在推荐一个专门的队列。

以上是关于IsolationLevel.RepeatableRead 防止重复的主要内容,如果未能解决你的问题,请参考以下文章