解决SQL事务超时的防弹方法

时间:2016-11-21 18:29:12

标签: c# .net sql-server transactions

最近我们遇到了一个与SQL事务超时有关的非常有趣的问题。超时的语句对于问题并不重要,但它是单个INSERT语句没有显式事务,客户端生成的GUID作为键:

INSERT MyTable
(id, ...)
VALUES (<client-app-generated-guid>, ...)

我们也有就地重试策略,因此如果命令失败并带有SqlException,那么它将被重试。 SQL Server(Azure SQL)有一天没有正常运行,我们在重试期间遇到了很多奇怪的PK违规错误。它们是由重试在SQL Server事务上实际成功提交引起的(因此导致插入已经获取的ID)。我理解SQL超时它是purely client side concept,所以如果客户端认为SqlCommand失败了 - 可能会或可能不会意味着它。

我怀疑客户端显式事务控制通过例如如下所示的TransactionScope包装语句将解决99%的此类问题 - 因为Commit实际上是非常快和廉价的操作。但是,我仍然看到那里的警告 - 超时也可能发生在提交阶段。该应用程序可能再次处于无法猜测事务是否真正承诺的情况下(以确定重试的必要性)。

问题是如何编写防弹(针对此类问题)和通用方式的代码,并且只有在明确表示未提交事务时才进行重试。

using (var trx = new TransactionScope())
using (var con = GetOpenConnection(connectionString))
{
    con.Execute("<some-non-idempotent-query>");

    // what if Complete() times out?!
    // to retry or not to retry?!
    trx.Complete();
}

2 个答案:

答案 0 :(得分:2)

问题是Exception并不意味着事务失败。对于任何补偿操作(如重试),您需要有一个确定的方法来告诉它是否失败。我会建议可扩展性问题,但其技术是重要的,可扩展性问题可以通过其他方式解决。

我的解决方案;

  1. COMMIT之前的最后一次INSERT是将Guid写入跟踪表。
  2. 如果发生异常,则表示网络出现故障,SELECT @@ TRANCOUNT。如果它表明你仍在交易中(大于0)(这可能永远不会发生,但值得检查)那么你可以高兴地重新提交你的COMMIT
  3. 如果@@ TRANCOUNT返回0,那么您不再处于事务中。从跟踪表中选择您的Guid将告诉您COMMIT是否成功。
  4. 如果您的提交不成功(@@ TRANCOUNT == 0并且您的Guid不在跟踪表中),请从BEGIN TRANSACTION开始重新提交您的整批产品。

答案 1 :(得分:2)

一般方法是:尝试回读您刚刚尝试插入的内容。

如果您可以回读您尝试插入的ID,那么之前的事务已成功提交,无需重试。

如果找不到您尝试插入的ID,则表示您尝试插入失败,因此您应该重试。

我担心没有办法可以使用适用于任何SQL语句的完全通用模式。您的“检查”代码需要知道要查找的内容。

如果是带有ID的INSERT - 那么您正在寻找该ID。

如果它是UPDATE,那么检查将是自定义的,并取决于UPDATE的性质。

如果是DELETE,则检查包括尝试阅读要删除的内容。

实际上,这是一个通用模式:任何包含一个或多个INSERTUPDATEDELETE语句的数据修改批处理中都应该有一个INSERT语句将某些GUID(数据修改事务本身的某个ID)插入专用审计表的事务。然后,您的检查代码会尝试从该专用审计表中读取相同的GUID。如果找到GUID,那么您知道先前的事务已成功提交。如果找不到GUID,那么您知道先前的事务已回滚,您可以重试。

使用此专用审计表统一/标准化检查。检查不再依赖于内部和数据更改代码的详细信息。您的数据修改代码和验证码取决于同一个商定的接口 - 审计表。