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

Yar*_*evi 7 sql transactions atomic sql-update

我试图了解如何安全地增加计数器列,这可能会被许多用户同时递增(它是移动应用程序的Web API).

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

UPDATE Table SET Counter = Counter + 1  
Run Code Online (Sandbox Code Playgroud)

我已经构建了以下代码示例来尝试获取不一致的值并证明自己只使用这个简单的更新语句并不是一个好习惯:

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();
                }
            }
        }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Lua*_*aan 4

如果您只使用这么简单的方法,那就没问题。

问题开始于以下情况:

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

当然,如果事务隔离级别发生变化,情况可能会完全不同。这实际上是错误的合理原因,因为 SQL 连接池不会重置事务隔离级别,因此如果您更改它,您需要确保它不会影响您在取出的数据库上执行的任何其他SqlConnectionSQL游泳池。