SQL 服务器中的死锁和无锁问题
Deadlocks and nolock issues in SQL Server
我们有一个存储过程可以插入和更新批量数据;我们在选择上使用 nolock
提示。现在当负载非常高时我们会遇到 601 错误。我们可以使用行版本控制吗?
如果是,可以使用哪个,因为我们同时有多个交易 运行。我们必须承受的另一件事是由于始终在线而导致的延迟,我们同步多个数据库服务器以处理数据库故障转移。
我们正在使用它,因为它可以防止读取被其他操作死锁。我们的应用程序事务处理率很高,我们担心的另一件事是我们始终使用 5 台服务器的可用性组,因此我们延迟提交,因为它仅在所有服务器中提交后才提交。那么我们应该使用行版本控制吗?如果是这样如何选择哪一个?或者我们可以进行快照隔离吗?除了 tempdb 内存使用之外,还需要承担哪些成本?
with (UPDLOCK, ROWLOCK) on 适用于小批量,但当批量高达 2000 时,它会陷入僵局。
这是执行SP的一段代码;相同的代码托管在 10 台服务器上,因此预计会同时 运行。 GetRSExecutionLogLatestID 是内部调用 SP 的地方。
ExecutionLogData execLog = this.fileShareDeliveryWrapper.GetRSExecutionLogLatestID(rptPath, notification.Owner, sharePointSubscriptiondata.RASSubscriptionID, this.configRSConnectionString);
if (execLog != null)
{
sharePointSubscriptiondata.RSExecutionLogId = execLog.RSExecutionLogId;
sharePointSubscriptiondata.RSSubscriptionId = execLog.RSSubscriptionId;
}
这是SP:
ALTER Procedure [dbo].[RAS_USP_GetLatestExecutionLogId]
@ReportURLPath NVARCHAR(MAX),
@UserID NVARCHAR(200),
@RASSubscriptionID NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
print @userId
DECLARE @SubscriptionId UNIQUEIDENTIFIER,@OwnerUserId UNIQUEIDENTIFIER, @LogEntryId BIGINT
DECLARE @Subscriptions AS TABLE(SubscriptionID uniqueIdentifier NOT NULL,
NotificationID uniqueidentifier NULL,
ReportID uniqueIdentifier NULL,
ExtensionSettings XML NOT NULL
)
INSERT INTO @Subscriptions
SELECT n.SubscriptionID,n.NotificationID,n.ReportID,n.ExtensionSettings
FROM dbo.Notifications AS n WITH (UPDLOCK, ROWLOCK) INNER JOIN
Subscriptions AS s WITH (UPDLOCK, ROWLOCK) ON n.SubscriptionID = s.SubscriptionID INNER JOIN
Catalog AS c WITH (UPDLOCK, ROWLOCK) ON c.ItemID = n.ReportID INNER JOIN
Users AS um WITH (UPDLOCK, ROWLOCK) ON um.UserID = s.OwnerID
WHERE c.[Path] = @ReportURLPath --AND um.UserName=@UserID
SELECT @SubscriptionID = SubScriptionID FROM
(SELECT SubscriptionID,
NotificationID,
ReportID,
pValues.pValue.value('Name[1]', 'VARCHAR(50)') AS ParamName,
pValues.pValue.value('Value[1]', 'VARCHAR(150)') AS ParamValue
FROM
@Subscriptions CROSS APPLY
ExtensionSettings.nodes('/ParameterValues/ParameterValue') pValues(pValue)
) AS Result
where ParamName like '%RASSubscriptionID%' AND ParamValue = CAST(@RASSubscriptionID AS VARCHAR(100))
SELECT @OwnerUserId = UserID FROM Users WHERE UserName = @UserID
SELECT @LogEntryId = LogEntryId FROM (
SELECT top 1 LogEntryId
FROM ExecutionLogStorage a WITH (UPDLOCK, ROWLOCK)
INNER JOIN [CATALOG] b WITH (UPDLOCK, ROWLOCK) ON a.reportid = b.itemid
INNER JOIN [Notifications] n WITH (UPDLOCK, ROWLOCK) ON n.ReportID = a.ReportID AND n.SubscriptionID = @SubscriptionId
INNER JOIN dbo.Subscriptions s WITH (UPDLOCK, ROWLOCK) ON s.SubscriptionID = n.SubscriptionID
WHERE [Path] = @ReportURLPath AND n.SubscriptionOwnerID=@OwnerUserId
ORDER BY TimeEnd desc) ss
SELECT @LogEntryId AS LogEntryId, @SubscriptionId AS SubscriptionID
END
当您可以控制查询并且需要语句之间的一致性时,SNAPSHOT ISOLATION
几乎是防止更新时阻塞的方法。 READ_COMMITTED_SNAPSHOT
是另一种可能性,但会影响所有查询,而且它不能保证在给定事务中 "consistency"(就像 READ COMMITTED
与 SERIALIZABLE
可以给出不同的结果一样)。 NOLOCK
在 99% 的情况下几乎都不是正确的选择。
您可以将 SNAPSHOT ISOLATION
视为可行的 "read-only" serializable
*。当然有几种成本——请注意,即使没有快照交易处于活动状态,您也会支付其中许多费用,因为当然,如果一个 变为 活动,则需要跟踪数据到位。
- tempdb 增加 activity,正如你提到的
- 行将占用更多space(每行 14 个字节)
- 大多数操作 activity 多一些,因此根据工作负载,性能可能会受到影响
其他注意事项:
- 最好不要从快照事务中更新数据,因为您可能会得到意想不到的结果
- 某些操作仍然无法完成(重建索引,例如 -- 重组是可以的,但至少在 SQL 2012 年,如果查询正在从快照隔离中的索引读取,同时它正在重建在其他地方,读取查询将失败)
- 无论出于何种愚蠢的原因,如果您使用连接池,当重用连接时隔离级别不会被重置,这可能会导致意外结果(但这适用于所有事务级别,而不仅仅是
SNAPSHOT
)
这篇文章很好的概括了情况:
https://technet.microsoft.com/en-us/library/ms188277(v=sql.105).aspx
但最终,如果您需要避免阻塞,唯一真正的选择是行版本控制和 NOLOCK,这不是一个选择。否则,您可能需要以不同的方式进行设计。其他可能性是使用 UPDLOCK 之类的东西来避免死锁,一次 insert/update 小批量数据,以便 "blocking periods" 是 short/unnoticeable,等等
(*) 我这样说是因为:
- 如果您打开一个
SNAPSHOT ISOLATION
事务并仅从中读取,您将获得与使用 SERIALIZABLE
相同的读取结果,只是您不会阻止写入而且你不会造成死锁
- 我说 "viable" 是因为大多数时候,引入
SERIALIZABLE
会导致太多的阻塞和太多的死锁。
我说 "read-only" 是因为尽管您 可以 在快照事务中写入,但这是一项复杂得多的工作。如果您仅从快照事务中 读取 ,则您不太可能 运行 陷入数据正确性问题——事实上,因为它类似于 SERIALIZABLE,您甚至可以提高一致性你的结果。您可能 运行 遇到的唯一问题是与性能相关的。因此,总的来说,将其引入此类查询是一项重大更改,风险相当低。不过,如果您开始写作:
- 如果您尝试保存更改,而同一数据已在另一个事务中更改,则更新将失败并显示错误 3960:
Snapshot isolation transaction aborted due to update conflict. You
cannot use snapshot isolation to access table 'XYZ' directly
or indirectly in database 'ABC' to update, delete, or insert
the row that has been modified or deleted by another transaction.
Retry the transaction or change the isolation level for the
update/delete statement.
这是你必须处理的新情况,比阻塞更困难(尽管我认为它与处理死锁的难度相似)。
- 即使你更新不同的数据,你仍然会得到不寻常的结果。来自 https://blogs.msdn.microsoft.com/craigfr/2007/05/16/serializable-vs-snapshot-isolation-level/ 的示例:
Imagine that we have a bag containing a mixture of white and black marbles. Suppose that we want to run two transactions. One
transaction turns each of the white marbles into black marbles. The
second transaction turns each of the black marbles into white marbles.
If we run these transactions under serializable isolation, we must run
them one at a time. The first transaction will leave a bag with
marbles of only one color. After that, the second transaction will
change all of these marbles to the other color. There are only two
possible outcomes: a bag with only white marbles or a bag with only
black marbles.
If we run these transactions under snapshot isolation, there is a
third outcome that is not possible under serializable isolation. Each
transaction can simultaneously take a snapshot of the bag of marbles
as it exists before we make any changes. Now one transaction finds
the white marbles and turns them into black marbles. At the same
time, the other transactions finds the black marbles – but only those
marbles that where black when we took the snapshot – not those marbles
that the first transaction changed to black – and turns them into
white marbles. In the end, we still have a mixed bag of marbles with
some white and some black. In fact, we have precisely switched each
marble.
换句话说,如果你坚持只从 SNAPSHOT 交易中读取,它就不那么复杂了,所以从这个意义上说,我说你可以认为它是可行的 "read-only" serializable
- - 如果你就此打住,它是一个有用的工具,几乎任何人都可以使用它来获得一致的、非阻塞的结果,无需使用 NOLOCK,只要他们能够承受性能损失。当然不止于此,但是要走得更远,需要处理的复杂度要高很多。
我们有一个存储过程可以插入和更新批量数据;我们在选择上使用 nolock
提示。现在当负载非常高时我们会遇到 601 错误。我们可以使用行版本控制吗?
如果是,可以使用哪个,因为我们同时有多个交易 运行。我们必须承受的另一件事是由于始终在线而导致的延迟,我们同步多个数据库服务器以处理数据库故障转移。
我们正在使用它,因为它可以防止读取被其他操作死锁。我们的应用程序事务处理率很高,我们担心的另一件事是我们始终使用 5 台服务器的可用性组,因此我们延迟提交,因为它仅在所有服务器中提交后才提交。那么我们应该使用行版本控制吗?如果是这样如何选择哪一个?或者我们可以进行快照隔离吗?除了 tempdb 内存使用之外,还需要承担哪些成本?
with (UPDLOCK, ROWLOCK) on 适用于小批量,但当批量高达 2000 时,它会陷入僵局。
这是执行SP的一段代码;相同的代码托管在 10 台服务器上,因此预计会同时 运行。 GetRSExecutionLogLatestID 是内部调用 SP 的地方。
ExecutionLogData execLog = this.fileShareDeliveryWrapper.GetRSExecutionLogLatestID(rptPath, notification.Owner, sharePointSubscriptiondata.RASSubscriptionID, this.configRSConnectionString);
if (execLog != null)
{
sharePointSubscriptiondata.RSExecutionLogId = execLog.RSExecutionLogId;
sharePointSubscriptiondata.RSSubscriptionId = execLog.RSSubscriptionId;
}
这是SP:
ALTER Procedure [dbo].[RAS_USP_GetLatestExecutionLogId]
@ReportURLPath NVARCHAR(MAX),
@UserID NVARCHAR(200),
@RASSubscriptionID NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
print @userId
DECLARE @SubscriptionId UNIQUEIDENTIFIER,@OwnerUserId UNIQUEIDENTIFIER, @LogEntryId BIGINT
DECLARE @Subscriptions AS TABLE(SubscriptionID uniqueIdentifier NOT NULL,
NotificationID uniqueidentifier NULL,
ReportID uniqueIdentifier NULL,
ExtensionSettings XML NOT NULL
)
INSERT INTO @Subscriptions
SELECT n.SubscriptionID,n.NotificationID,n.ReportID,n.ExtensionSettings
FROM dbo.Notifications AS n WITH (UPDLOCK, ROWLOCK) INNER JOIN
Subscriptions AS s WITH (UPDLOCK, ROWLOCK) ON n.SubscriptionID = s.SubscriptionID INNER JOIN
Catalog AS c WITH (UPDLOCK, ROWLOCK) ON c.ItemID = n.ReportID INNER JOIN
Users AS um WITH (UPDLOCK, ROWLOCK) ON um.UserID = s.OwnerID
WHERE c.[Path] = @ReportURLPath --AND um.UserName=@UserID
SELECT @SubscriptionID = SubScriptionID FROM
(SELECT SubscriptionID,
NotificationID,
ReportID,
pValues.pValue.value('Name[1]', 'VARCHAR(50)') AS ParamName,
pValues.pValue.value('Value[1]', 'VARCHAR(150)') AS ParamValue
FROM
@Subscriptions CROSS APPLY
ExtensionSettings.nodes('/ParameterValues/ParameterValue') pValues(pValue)
) AS Result
where ParamName like '%RASSubscriptionID%' AND ParamValue = CAST(@RASSubscriptionID AS VARCHAR(100))
SELECT @OwnerUserId = UserID FROM Users WHERE UserName = @UserID
SELECT @LogEntryId = LogEntryId FROM (
SELECT top 1 LogEntryId
FROM ExecutionLogStorage a WITH (UPDLOCK, ROWLOCK)
INNER JOIN [CATALOG] b WITH (UPDLOCK, ROWLOCK) ON a.reportid = b.itemid
INNER JOIN [Notifications] n WITH (UPDLOCK, ROWLOCK) ON n.ReportID = a.ReportID AND n.SubscriptionID = @SubscriptionId
INNER JOIN dbo.Subscriptions s WITH (UPDLOCK, ROWLOCK) ON s.SubscriptionID = n.SubscriptionID
WHERE [Path] = @ReportURLPath AND n.SubscriptionOwnerID=@OwnerUserId
ORDER BY TimeEnd desc) ss
SELECT @LogEntryId AS LogEntryId, @SubscriptionId AS SubscriptionID
END
SNAPSHOT ISOLATION
几乎是防止更新时阻塞的方法。 READ_COMMITTED_SNAPSHOT
是另一种可能性,但会影响所有查询,而且它不能保证在给定事务中 "consistency"(就像 READ COMMITTED
与 SERIALIZABLE
可以给出不同的结果一样)。 NOLOCK
在 99% 的情况下几乎都不是正确的选择。
您可以将 SNAPSHOT ISOLATION
视为可行的 "read-only" serializable
*。当然有几种成本——请注意,即使没有快照交易处于活动状态,您也会支付其中许多费用,因为当然,如果一个 变为 活动,则需要跟踪数据到位。
- tempdb 增加 activity,正如你提到的
- 行将占用更多space(每行 14 个字节)
- 大多数操作 activity 多一些,因此根据工作负载,性能可能会受到影响
其他注意事项:
- 最好不要从快照事务中更新数据,因为您可能会得到意想不到的结果
- 某些操作仍然无法完成(重建索引,例如 -- 重组是可以的,但至少在 SQL 2012 年,如果查询正在从快照隔离中的索引读取,同时它正在重建在其他地方,读取查询将失败)
- 无论出于何种愚蠢的原因,如果您使用连接池,当重用连接时隔离级别不会被重置,这可能会导致意外结果(但这适用于所有事务级别,而不仅仅是
SNAPSHOT
)
这篇文章很好的概括了情况: https://technet.microsoft.com/en-us/library/ms188277(v=sql.105).aspx
但最终,如果您需要避免阻塞,唯一真正的选择是行版本控制和 NOLOCK,这不是一个选择。否则,您可能需要以不同的方式进行设计。其他可能性是使用 UPDLOCK 之类的东西来避免死锁,一次 insert/update 小批量数据,以便 "blocking periods" 是 short/unnoticeable,等等
(*) 我这样说是因为:
- 如果您打开一个
SNAPSHOT ISOLATION
事务并仅从中读取,您将获得与使用SERIALIZABLE
相同的读取结果,只是您不会阻止写入而且你不会造成死锁 - 我说 "viable" 是因为大多数时候,引入
SERIALIZABLE
会导致太多的阻塞和太多的死锁。 我说 "read-only" 是因为尽管您 可以 在快照事务中写入,但这是一项复杂得多的工作。如果您仅从快照事务中 读取 ,则您不太可能 运行 陷入数据正确性问题——事实上,因为它类似于 SERIALIZABLE,您甚至可以提高一致性你的结果。您可能 运行 遇到的唯一问题是与性能相关的。因此,总的来说,将其引入此类查询是一项重大更改,风险相当低。不过,如果您开始写作:
- 如果您尝试保存更改,而同一数据已在另一个事务中更改,则更新将失败并显示错误 3960:
Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'XYZ' directly or indirectly in database 'ABC' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.
这是你必须处理的新情况,比阻塞更困难(尽管我认为它与处理死锁的难度相似)。
- 即使你更新不同的数据,你仍然会得到不寻常的结果。来自 https://blogs.msdn.microsoft.com/craigfr/2007/05/16/serializable-vs-snapshot-isolation-level/ 的示例:
Imagine that we have a bag containing a mixture of white and black marbles. Suppose that we want to run two transactions. One transaction turns each of the white marbles into black marbles. The second transaction turns each of the black marbles into white marbles. If we run these transactions under serializable isolation, we must run them one at a time. The first transaction will leave a bag with marbles of only one color. After that, the second transaction will change all of these marbles to the other color. There are only two possible outcomes: a bag with only white marbles or a bag with only black marbles.
If we run these transactions under snapshot isolation, there is a third outcome that is not possible under serializable isolation. Each transaction can simultaneously take a snapshot of the bag of marbles as it exists before we make any changes. Now one transaction finds the white marbles and turns them into black marbles. At the same time, the other transactions finds the black marbles – but only those marbles that where black when we took the snapshot – not those marbles that the first transaction changed to black – and turns them into white marbles. In the end, we still have a mixed bag of marbles with some white and some black. In fact, we have precisely switched each marble.
换句话说,如果你坚持只从 SNAPSHOT 交易中读取,它就不那么复杂了,所以从这个意义上说,我说你可以认为它是可行的 "read-only" serializable
- - 如果你就此打住,它是一个有用的工具,几乎任何人都可以使用它来获得一致的、非阻塞的结果,无需使用 NOLOCK,只要他们能够承受性能损失。当然不止于此,但是要走得更远,需要处理的复杂度要高很多。