使用简单更新的计数器列的原子增量

Atomic increment of counter column using simple update

我正在尝试了解如何安全地递增计数器列,该列可能会被许多用户同时递增(它是用于移动应用程序的 Web API)。

我已经阅读了 SO 中的热门问题以了解处理该问题的策略,但我似乎无法弄清楚使用简单的方法有什么问题:

UPDATE Table SET Counter = Counter + 1  

我构建了以下代码示例来尝试获取不一致的值并证明仅使用这个简单的更新语句不是好的做法:

class Program
{
    static void Main(string[] args)
        {
            List<Task> tasks = new List<Task>();

            for (int i = 0; i < 100; i++)
            {
                Task t = Task.Factory.StartNew(() =>
                {
                    WriteToCounter();
                });

                tasks.Add(t);
            }

            Task.WaitAll(tasks.ToArray());
        }

    static void WriteToCounter()
        {
            string connString = ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;

            using (SqlConnection connection = new SqlConnection(connString))
            {
                connection.Open();
                Random rnd = new Random();
                for (int i = 1; i <= 100; i++)
                {
                    int wait = rnd.Next(1, 3);
                    Thread.Sleep(wait);

                    string sql = "UPDATE Table SET Counter = Counter + 1";

                    SqlCommand command = new SqlCommand(sql, connection);
                    command.ExecuteNonQuery();
                }
            }
        }
}

在示例中,我尝试模拟许多用户同时访问 API 并更新计数器的场景。代码运行时,计数器始终为10000,这意味着它是一致的。

测试是否正确模拟了我描述的场景?
如果是这样,为什么我可以在没有任何特殊 locking/transaction 策略的情况下使用更新语句并仍然获得一致的结果?

如果您只是像这样简单地使用它,就可以了。

问题开始于:

  • 您添加一个条件 - 大多数条件都可以,但避免基于 Counter 进行过滤,这是失去确定性的好方法
  • 您在 事务 内部进行更新(注意这一点 - 很容易处于实际更新语句范围之外的事务中,如果您使用例如TransactionScope)
  • 您结合了插入和更新(例如通常的 "insert if not exists" 模式)- 如果您只有一个计数器,这不是问题,但对于多个计数器,很容易陷入这个陷阱;解决起来并不难,除非你也有删除,否则它就变成了一个完全不同的联盟:)
  • 可能 如果您依赖 Counter 的值作为唯一的自动递增标识符。如果您将 selectupdate 分开(不,update based on select 显然不起作用帮助 - 与普通 update 不同,select 未在同一行上进行更新序列化;这就是锁定提示的用武之地),我不确定使用 output 是否安全。

当然,如果事务隔离级别发生变化,情况可能会大不相同。这实际上是一个合法的错误原因,因为 SQL 连接池不会重置事务隔离级别,所以如果你改变它,你需要确保它不会影响任何其他 SQL 你在从池中取出的 SqlConnection 上执行。

how come the I can use the update statement without any special locking/transaction strategies and still get consistent results?

因为当您使用提供 ACID 保证的数据库时,您会自动获得许多这些功能。

例如,每DML query runs inside a transaction. In SQL Server, the default is for it to run in autocommit mode. In this mode, if you execute a query and there is no open transaction, it creates a new one. If the query completes without error, it automatically commits the transaction. In an alternative mode called implicit transactions,如果没有打开的事务,它仍然会自动创建一个新的事务,但它让用户决定是否实际执行提交。

至于 locks,这里也有相当多的复杂性。有多种形式的锁,试图在允许并发和防止出现不一致之间取得权衡。而且,事实上,SQL 服务器有一种专用类型的锁,仅用于 UPDATEs,旨在确保对 UPDATE 相同资源的两次并行尝试将被正确序列化(而不是允许尝试重叠和潜在的死锁)。

所以,长话短说,您在问题中显示的 UPDATE 完全有效。