最近我们遇到了一个与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();
}
答案 0 :(得分:2)
问题是Exception并不意味着事务失败。对于任何补偿操作(如重试),您需要有一个确定的方法来告诉它是否失败。我会建议可扩展性问题,但其技术是重要的,可扩展性问题可以通过其他方式解决。
我的解决方案;
答案 1 :(得分:2)
一般方法是:尝试回读您刚刚尝试插入的内容。
如果您可以回读您尝试插入的ID,那么之前的事务已成功提交,无需重试。
如果找不到您尝试插入的ID,则表示您尝试插入失败,因此您应该重试。
我担心没有办法可以使用适用于任何SQL语句的完全通用模式。您的“检查”代码需要知道要查找的内容。
如果是带有ID的INSERT
- 那么您正在寻找该ID。
如果它是UPDATE
,那么检查将是自定义的,并取决于UPDATE
的性质。
如果是DELETE
,则检查包括尝试阅读要删除的内容。
实际上,这是一个通用模式:任何包含一个或多个INSERT
,UPDATE
,DELETE
语句的数据修改批处理中都应该有一个INSERT
语句将某些GUID(数据修改事务本身的某个ID)插入专用审计表的事务。然后,您的检查代码会尝试从该专用审计表中读取相同的GUID。如果找到GUID,那么您知道先前的事务已成功提交。如果找不到GUID,那么您知道先前的事务已回滚,您可以重试。
使用此专用审计表统一/标准化检查。检查不再依赖于内部和数据更改代码的详细信息。您的数据修改代码和验证码取决于同一个商定的接口 - 审计表。