SQL 服务器仅强制 RowLock
SQL Server force only RowLock
我在更新存储计数器的 table 的行时遇到了一个大问题。我正在使用一个事务来获取值并更新它,我尝试只锁定受影响的计数器行以避免锁定和死锁,但它不起作用。
这是一个简化的代码示例,我可以在其中重现错误:
CREATE TABLE _COUNTERS_
(
ID INT IDENTITY NOT NULL,
CODE VARCHAR(20) NOT NULL,
CVALUE INT NOT NULL DEFAULT 0,
CONSTRAINT PK_ID PRIMARY KEY CLUSTERED
(
ID ASC,
CODE ASC,
CVALUE ASC
) WITH (ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF)
)
INSERT INTO _COUNTERS_ (CODE, CVALUE) VALUES ('C1', 0)
INSERT INTO _COUNTERS_ (CODE, CVALUE) VALUES ('C2', 0)
我试图强制避免使用索引定义的 PageLocks。
在 SQL Server Management Studio 中,我在 window 中执行此语句。我使用第一个查询来锁定 COUNTER 以避免服务器上的其他线程(应用程序是多线程的并且安装在农场上所以我不能使用 .NET 锁)在接近更新时得到一个错误的数字 num:
-- Window 1
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C1'
UPDATE _COUNTERS_ SET CVALUE = 1 WHERE ID = 1
在另一个 SQL Server Management Studio window 中,我查询锁定的资源:
-- Window 2
SELECT L.request_session_id AS SPID,
-- DB_NAME(L.resource_database_id) AS DatabaseName,
O.Name AS LockedObjectName,
P.object_id AS LockedObjectId,
L.resource_type AS LockedResource,
L.request_mode AS LockType,
ST.text AS SqlStatementText,
-- ES.login_name AS LoginName,
-- ES.host_name AS HostName,
TST.is_user_transaction as IsUserTransaction,
AT.name as TransactionName,
CN.auth_scheme as AuthenticationMethod
FROM sys.dm_tran_locks L
JOIN sys.partitions P ON P.hobt_id = L.resource_associated_entity_id
JOIN sys.objects O ON O.object_id = P.object_id
JOIN sys.dm_exec_sessions ES ON ES.session_id = L.request_session_id
JOIN sys.dm_tran_session_transactions TST ON ES.session_id = TST.session_id
JOIN sys.dm_tran_active_transactions AT ON TST.transaction_id = AT.transaction_id
JOIN sys.dm_exec_connections CN ON CN.session_id = ES.session_id
CROSS APPLY sys.dm_exec_sql_text(CN.most_recent_sql_handle) AS ST
WHERE resource_database_id = db_id()
ORDER BY L.request_session_id
在第三个 window 中,我执行以下语句:
-- Window 3
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2'
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
第一个 select 一直等到我提交第一个事务。
是否可以在 selects 和更新中保持每个计数器完全隔离?
关于真实环境的一些额外注意事项:
- SQL 服务器 2012
- table 有 107 行
- 第一笔交易 (Window 1) 仅在锁查询中显示一行,而不是我发布的屏幕截图中的两行。
- 第二个事务 (Window 3),可以执行第一个 select 而无需等待,它一直在等待 UPDATE 语句。
- 首先执行两个 select 查询然后执行两个更新重现死锁(仅在真实环境中),它不能用发布的示例重现,因为第一个 select 锁已满 table.
更新 2020-03-02
使用代码索引(由@larny 评论或@esat 发布)解决了我发布的示例中的问题,但在我的真实 table (VISUALSEGCONTADORES) 中,select 没有使用新索引:
CREATE INDEX ix_VSGCONTADORES ON VISUALSEGCONTADORES (VSC_ALIAS ASC) WITH (ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF);
这里是select之一的计划(另一个使用相同的索引):https://www.brentozar.com/pastetheplan/?id=BJRWtocE8
而真正的table结构(带有特殊聚集索引):
CREATE TABLE [dbo].[VISUALSEGCONTADORES](
[VSC_Id] [int] IDENTITY(1,1) NOT NULL,
[VSC_Alias] [varchar](30) NOT NULL,
[VSC_Objeto] [int] NOT NULL,
[VSC_Serie] [varchar](5) NULL,
[VSC_Contador] [decimal](20, 8) NOT NULL,
[VSC_Enabled] [tinyint] NOT NULL,
[USR_Id_FC] [int] NOT NULL,
[USR_Id_FM] [int] NOT NULL,
[VSC_FC] [datetime] NOT NULL,
[VSC_FM] [datetime] NOT NULL,
[LOG_ID_FC] [varchar](255) NULL,
[LOG_ID_FM] [varchar](255) NULL,
[LOG_FC] [datetime] NULL,
[LOG_FM] [datetime] NULL,
[OFI_ID] [int] NULL,
[VSC_OFICODE] [int] NOT NULL,
[TRN_Aud_Id_FC] [int] NULL,
[TRN_Aud_Id] [int] NULL,
CONSTRAINT [PK_VISUALSEGCONTADORES] PRIMARY KEY CLUSTERED
(
[VSC_Alias] ASC,
[VSC_OFICODE] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY]
) ON [PRIMARY]
真实查询如下:
-- Window 1
BEGIN TRAN
SELECT *
FROM VISUALSEGCONTADORES this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.VSC_Alias = 'VSG_UltimoCodEXP' AND VSC_OFICODE = 0
UPDATE VISUALSEGCONTADORES SET VSC_Contador = 9910 WHERE VSC_Id = 142
其他window
-- Window 3
BEGIN TRAN
SELECT *
FROM VISUALSEGCONTADORES this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.VSC_Alias = 'VSG_ULTIMAMATRIZ' AND VSC_OFICODE = 0
UPDATE VISUALSEGCONTADORES SET VSC_Contador = 1273 WHERE VSC_Id = 121
由业务定义,有时查询带有 VSC_OfiCode,有时则没有,但我已经测试了两者并抛出相同的结果。
锁定的资源:
Window3 查询试图获取对 Window1 仍在读取的行的独占锁定。
如果分析下面查询的执行。
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2'
所以这个查询将等到 Window1 查询提交或回滚。如果您尝试单独执行Window3更新,它将被执行
-- Window 3
BEGIN TRAN
--SELECT *
--FROM _COUNTERS_ this_ WITH (
-- UPDLOCK,
-- ROWLOCK
-- )
--WHERE this_.CODE = 'C2'
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
另一方面,如果我们处理这个问题,我们可以在 CODE 列上创建一个非聚集索引,或者我们可以像下面这样更改查询
--Window3
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2' AND ID=2
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
这两个选项提供避免集群索引扫描
我在更新存储计数器的 table 的行时遇到了一个大问题。我正在使用一个事务来获取值并更新它,我尝试只锁定受影响的计数器行以避免锁定和死锁,但它不起作用。
这是一个简化的代码示例,我可以在其中重现错误:
CREATE TABLE _COUNTERS_
(
ID INT IDENTITY NOT NULL,
CODE VARCHAR(20) NOT NULL,
CVALUE INT NOT NULL DEFAULT 0,
CONSTRAINT PK_ID PRIMARY KEY CLUSTERED
(
ID ASC,
CODE ASC,
CVALUE ASC
) WITH (ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF)
)
INSERT INTO _COUNTERS_ (CODE, CVALUE) VALUES ('C1', 0)
INSERT INTO _COUNTERS_ (CODE, CVALUE) VALUES ('C2', 0)
我试图强制避免使用索引定义的 PageLocks。
在 SQL Server Management Studio 中,我在 window 中执行此语句。我使用第一个查询来锁定 COUNTER 以避免服务器上的其他线程(应用程序是多线程的并且安装在农场上所以我不能使用 .NET 锁)在接近更新时得到一个错误的数字 num:
-- Window 1
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C1'
UPDATE _COUNTERS_ SET CVALUE = 1 WHERE ID = 1
在另一个 SQL Server Management Studio window 中,我查询锁定的资源:
-- Window 2
SELECT L.request_session_id AS SPID,
-- DB_NAME(L.resource_database_id) AS DatabaseName,
O.Name AS LockedObjectName,
P.object_id AS LockedObjectId,
L.resource_type AS LockedResource,
L.request_mode AS LockType,
ST.text AS SqlStatementText,
-- ES.login_name AS LoginName,
-- ES.host_name AS HostName,
TST.is_user_transaction as IsUserTransaction,
AT.name as TransactionName,
CN.auth_scheme as AuthenticationMethod
FROM sys.dm_tran_locks L
JOIN sys.partitions P ON P.hobt_id = L.resource_associated_entity_id
JOIN sys.objects O ON O.object_id = P.object_id
JOIN sys.dm_exec_sessions ES ON ES.session_id = L.request_session_id
JOIN sys.dm_tran_session_transactions TST ON ES.session_id = TST.session_id
JOIN sys.dm_tran_active_transactions AT ON TST.transaction_id = AT.transaction_id
JOIN sys.dm_exec_connections CN ON CN.session_id = ES.session_id
CROSS APPLY sys.dm_exec_sql_text(CN.most_recent_sql_handle) AS ST
WHERE resource_database_id = db_id()
ORDER BY L.request_session_id
在第三个 window 中,我执行以下语句:
-- Window 3
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2'
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
第一个 select 一直等到我提交第一个事务。
是否可以在 selects 和更新中保持每个计数器完全隔离?
关于真实环境的一些额外注意事项:
- SQL 服务器 2012
- table 有 107 行
- 第一笔交易 (Window 1) 仅在锁查询中显示一行,而不是我发布的屏幕截图中的两行。
- 第二个事务 (Window 3),可以执行第一个 select 而无需等待,它一直在等待 UPDATE 语句。
- 首先执行两个 select 查询然后执行两个更新重现死锁(仅在真实环境中),它不能用发布的示例重现,因为第一个 select 锁已满 table.
更新 2020-03-02 使用代码索引(由@larny 评论或@esat 发布)解决了我发布的示例中的问题,但在我的真实 table (VISUALSEGCONTADORES) 中,select 没有使用新索引:
CREATE INDEX ix_VSGCONTADORES ON VISUALSEGCONTADORES (VSC_ALIAS ASC) WITH (ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF);
这里是select之一的计划(另一个使用相同的索引):https://www.brentozar.com/pastetheplan/?id=BJRWtocE8
而真正的table结构(带有特殊聚集索引):
CREATE TABLE [dbo].[VISUALSEGCONTADORES](
[VSC_Id] [int] IDENTITY(1,1) NOT NULL,
[VSC_Alias] [varchar](30) NOT NULL,
[VSC_Objeto] [int] NOT NULL,
[VSC_Serie] [varchar](5) NULL,
[VSC_Contador] [decimal](20, 8) NOT NULL,
[VSC_Enabled] [tinyint] NOT NULL,
[USR_Id_FC] [int] NOT NULL,
[USR_Id_FM] [int] NOT NULL,
[VSC_FC] [datetime] NOT NULL,
[VSC_FM] [datetime] NOT NULL,
[LOG_ID_FC] [varchar](255) NULL,
[LOG_ID_FM] [varchar](255) NULL,
[LOG_FC] [datetime] NULL,
[LOG_FM] [datetime] NULL,
[OFI_ID] [int] NULL,
[VSC_OFICODE] [int] NOT NULL,
[TRN_Aud_Id_FC] [int] NULL,
[TRN_Aud_Id] [int] NULL,
CONSTRAINT [PK_VISUALSEGCONTADORES] PRIMARY KEY CLUSTERED
(
[VSC_Alias] ASC,
[VSC_OFICODE] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF) ON [PRIMARY]
) ON [PRIMARY]
真实查询如下:
-- Window 1
BEGIN TRAN
SELECT *
FROM VISUALSEGCONTADORES this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.VSC_Alias = 'VSG_UltimoCodEXP' AND VSC_OFICODE = 0
UPDATE VISUALSEGCONTADORES SET VSC_Contador = 9910 WHERE VSC_Id = 142
其他window
-- Window 3
BEGIN TRAN
SELECT *
FROM VISUALSEGCONTADORES this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.VSC_Alias = 'VSG_ULTIMAMATRIZ' AND VSC_OFICODE = 0
UPDATE VISUALSEGCONTADORES SET VSC_Contador = 1273 WHERE VSC_Id = 121
由业务定义,有时查询带有 VSC_OfiCode,有时则没有,但我已经测试了两者并抛出相同的结果。
锁定的资源:
Window3 查询试图获取对 Window1 仍在读取的行的独占锁定。 如果分析下面查询的执行。
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2'
所以这个查询将等到 Window1 查询提交或回滚。如果您尝试单独执行Window3更新,它将被执行
-- Window 3
BEGIN TRAN
--SELECT *
--FROM _COUNTERS_ this_ WITH (
-- UPDLOCK,
-- ROWLOCK
-- )
--WHERE this_.CODE = 'C2'
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
另一方面,如果我们处理这个问题,我们可以在 CODE 列上创建一个非聚集索引,或者我们可以像下面这样更改查询
--Window3
BEGIN TRAN
SELECT *
FROM _COUNTERS_ this_ WITH (
UPDLOCK,
ROWLOCK
)
WHERE this_.CODE = 'C2' AND ID=2
UPDATE _COUNTERS_ SET CVALUE = 2 WHERE ID = 2
这两个选项提供避免集群索引扫描