如何让 SQL Server 事务使用记录级锁?
我们有一个应用程序最初是作为桌面应用程序编写的,很多年前。每当您打开编辑屏幕时,它都会启动一个事务,如果单击“确定”则提交,如果单击“取消”则回滚。这对于桌面应用程序来说效果很好,但现在我们正在尝试迁移到 ADO.NET 和 SQL Server,并且长时间运行的事务会出现问题。
我发现当多个用户都尝试同时编辑同一个表(的不同子集)时,我们会遇到问题。在我们的旧数据库中,每个用户的事务都会为其在事务期间修改的每条记录获取记录级锁;由于不同的用户编辑不同的记录,每个人都有自己的锁,一切正常。但在 SQL Server 中,一旦一个用户在事务中编辑一条记录,SQL Server 就会似乎获得整个表的锁定。当第二个用户尝试编辑同一个表中的不同记录时,第二个用户的应用程序只是锁定,因为 SqlConnection 会阻塞,直到第一个用户提交或回滚。
我知道长时间运行的事务是不好的,并且我知道最好的解决方案是更改这些屏幕,以便它们不再长时间保持事务打开。但由于这意味着一些侵入性和危险的变化,我还想研究是否有办法让这段代码按原样启动并运行,这样我就知道我的选择是什么。
如何让 SQL Server 中两个不同用户的事务锁定单个记录而不是整个表?
下面是一个快速但简单的控制台应用程序,它说明了该问题。我创建了一个名为“test1”的数据库,其中有一个名为“Values”的表,该表仅包含 ID (int) 和 Value (nvarchar) 列。如果您运行该应用程序,它会要求输入要修改的 ID,启动事务,修改该记录,然后使事务保持打开状态,直到您按 ENTER 键。我希望能够
- 启动程序并告诉它更新 ID 1;
- 让它获取交易并修改记录;
- 启动该程序的第二个副本并告诉它更新 ID 2;
- 让它能够在第一个应用程序的事务仍然打开时更新(并提交)。
目前它在第 4 步冻结,直到我返回应用程序的第一个副本并将其关闭或按 ENTER 键以便它提交。对 command.ExecuteNonQuery 的调用会阻塞,直到第一个连接关闭。
public static void Main()
{
Console.Write("ID to update: ");
var id = int.Parse(Console.ReadLine());
Console.WriteLine("Starting transaction");
using (var scope = new TransactionScope())
using (var connection = new SqlConnection(@"Data Source=localhost\sqlexpress;Initial Catalog=test1;Integrated Security=True"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "UPDATE [Values] SET Value = 'Value' WHERE ID = " + id;
Console.WriteLine("Updating record");
command.ExecuteNonQuery();
Console.Write("Press ENTER to end transaction: ");
Console.ReadLine();
scope.Complete();
}
}
以下是我已经尝试过的一些操作,但行为没有改变:
- 将事务隔离级别更改为“未提交读”
- 在 UPDATE 语句上指定“WITH (ROWLOCK)”
We have an application that was originally written as a desktop app, lo these many years ago. It starts a transaction whenever you open an edit screen, and commits if you click OK, or rolls back if you click Cancel. This worked okay for a desktop app, but now we're trying to move to ADO.NET and SQL Server, and the long-running transactions are problematic.
I found that we'll have a problem when multiple users are all trying to edit (different subsets of) the same table at the same time. In our old database, each user's transaction would acquire record-level locks to every record they modified during their transaction; since different users were editing different records, everyone gets their own locks and everything works. But in SQL Server, as soon as one user edits a record inside a transaction, SQL Server appears to get a lock on the entire table. When a second user tries to edit a different record in the same table, the second user's app simply locks up, because the SqlConnection blocks until the first user either commits or rolls back.
I'm aware that long-running transactions are bad, and I know that the best solution would be to change these screens so that they no longer keep transactions open for a long time. But since that would mean some invasive and risky changes, I also want to research whether there's a way to get this code up and running as-is, just so I know what my options are.
How can I get two different users' transactions in SQL Server to lock individual records instead of the entire table?
Here's a quick-and-dirty console app that illustrates the issue. I've created a database called "test1", with one table called "Values" that just has ID (int) and Value (nvarchar) columns. If you run the app, it asks for an ID to modify, starts a transaction, modifies that record, and then leaves the transaction open until you press ENTER. I want to be able to
- start the program and tell it to update ID 1;
- let it get its transaction and modify the record;
- start a second copy of the program and tell it to update ID 2;
- have it able to update (and commit) while the first app's transaction is still open.
Currently it freezes at step 4, until I go back to the first copy of the app and close it or press ENTER so it commits. The call to command.ExecuteNonQuery blocks until the first connection is closed.
public static void Main()
{
Console.Write("ID to update: ");
var id = int.Parse(Console.ReadLine());
Console.WriteLine("Starting transaction");
using (var scope = new TransactionScope())
using (var connection = new SqlConnection(@"Data Source=localhost\sqlexpress;Initial Catalog=test1;Integrated Security=True"))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "UPDATE [Values] SET Value = 'Value' WHERE ID = " + id;
Console.WriteLine("Updating record");
command.ExecuteNonQuery();
Console.Write("Press ENTER to end transaction: ");
Console.ReadLine();
scope.Complete();
}
}
Here are some things I've already tried, with no change in behavior:
- Changing the transaction isolation level to "read uncommitted"
- Specifying a "WITH (ROWLOCK)" on the UPDATE statement
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
只是检查一下,但 ID 列上是否有主键或唯一索引?
Just checking, but do you have a primary key or unique index on the ID column?
研究乐观锁定与悲观锁定。
编辑:
上一篇文章链接到经典的 ado...抱歉。
http://msdn.microsoft.com/en-us /library/cs6hb8k4(VS.71).aspx
Look into optimistic versus pessimistic locking.
Edit:
Previous article linked to classic ado...sorry.
http://msdn.microsoft.com/en-us/library/cs6hb8k4(VS.71).aspx
可能该索引是在行锁设置为“关闭”的情况下创建的。
在这种情况下,查询中的“WITH (ROWLOCK)”将不起作用。
您可以使用ALTER INDEX重新打开它们,例如:
Probably the index was created with row locks set to "off".
"WITH (ROWLOCK)" in a query would have no effect in that case.
You can turn them back on with ALTER INDEX, e.g.: