序
本文屬于基礎知識的回顧,在日常技術交流和日常工作中經常發(fā)現(xiàn)有些同事了解關于數(shù)據庫事務的基本知識,會看SQL語句的執(zhí)行計劃,也知道數(shù)據庫有X鎖、U鎖和S鎖等各種鎖,但是對于這些鎖在數(shù)據庫事務執(zhí)行期間是如何工作?為何這樣配合才能完成數(shù)據庫事務?數(shù)據庫是如何對于各種資源加鎖的?等等這類的問題不太了解,那么對于事務的執(zhí)行肯定不會有深刻的認識。
這類知識雖然從網上搜索可以找到很多,但是大多內容重復,并且只注重理論知識而沒有實踐路徑。就好比池塘中的青蓮只可遠觀而無法靠近仔細觀察,猶如霧里看花水中望月,對于其真實原理總是似懂非懂。
紙上得來終覺淺,絕知此事要躬行,只有親自動手進行分析才能對這些問題有深入的認識,因此本文計劃從數(shù)據庫的基礎知識入手,以詳細的實踐分析步驟引導認識數(shù)據庫事務的執(zhí)行過程,以期讀者可以對于事務有更加深刻的理解。
SQL Server使用的鎖及鎖對象
數(shù)據庫引擎使用不同的鎖模式鎖定資源,通過不同鎖的組合使用達到不同的數(shù)據庫事務隔離級別。
鎖模式 |
編號 |
效果說明 |
共享鎖 |
S |
共享鎖,通常用于不修改數(shù)據也不希望數(shù)據被修改的場景 |
更新鎖 |
U |
用于可更新的資源,防止這類資源在讀取、鎖定以及隨后可能進行的資源更新時出現(xiàn)死鎖 |
排他鎖 |
X |
用于修改數(shù)據的操作,例如insert、update和delete,防止對同一個資源進行多重修改 |
意向鎖 |
|
包括意向共享、意向更新和意向排他三種,用于保護較低級別的鎖并提升性能 |
架構鎖 |
|
用于執(zhí)行依賴表結構的操作時使用,包括架構修改 (Sch-M) 和架構穩(wěn)定性 (Sch-S) |
大容量更新 |
BU |
在將數(shù)據大容量復制到表中且指定了 TABLOCK 提示時使用 |
鍵范圍 |
|
當使用可序列化事務隔離級別時保護查詢讀取的行的范圍。 |
意向鎖又細分為多種類型:
鎖模式 |
編號 |
效果說明 |
意向共享 |
IS |
保護針對底層資源的共享鎖 |
意向排他 |
IX |
保護針對底層資源的排他鎖是,IS的超集 |
共享意向排他 |
SIX |
保護針對低層資源請求或獲取的意向排他鎖以意向共享鎖 |
意向更新 |
IU |
保護針對底層資源的更新鎖 |
共享意向更新 |
SIU |
S鎖和IU鎖的組合,作為分別獲取并同時具備兩種鎖的組合效果 |
更新意向排他 |
UIX |
U鎖和IX鎖的組合,作為分別獲取并同時具備兩種鎖的組合效果 |
架構鎖細分為兩種類型:
鎖模式 |
編號 |
效果說明 |
架構修改鎖 |
Sch-M |
DDL執(zhí)行期間使用架構修改鎖,該鎖會阻止對于表的所有訪問 |
架構穩(wěn)定鎖 |
Sch-S |
該鎖不會影響S、U以及X鎖的執(zhí)行,但是會阻止DDL的執(zhí)行 |
通常開發(fā)人員談到數(shù)據庫的鎖的時候習慣說數(shù)據庫鎖、表鎖或者行鎖。這種描述通常是從被鎖定資源的角度來談論,通過檢索SQL Server2016的文檔發(fā)現(xiàn)數(shù)據庫上鎖定更多的資源不只是這三種維度,還有11種類型。
鎖對象 |
關于鎖對象的說明 |
AllocUnit |
分配單元 |
Application |
應用程序專用的資源 |
Database |
整個數(shù)據庫 |
Extent |
一組連續(xù)的8個頁 |
File |
數(shù)據庫文件 |
Heap/B-tree |
堆或者B樹 |
Key |
索引上的某一行 |
Metadata |
元數(shù)據 |
Object |
表、存儲過程、視圖等包括所有的數(shù)據和索引 |
OIB |
用于聯(lián)機索引構建時的鎖 |
Page |
數(shù)據庫上8KB頁 |
RID |
堆上的某一行 |
RowGroup |
列存儲索引行組的時候使用的鎖 |
Xact |
事務的鎖定資源 |
了解了數(shù)據庫的鎖及其鎖定對象,那么日常使用的select、insert和update語句到底是如何應用這些概念呢?
SQL Server執(zhí)行Select時使用的鎖
首先通過建表腳本創(chuàng)建一個數(shù)據庫表:
USE [Test]
GO
/****** Object: Table [dbo].[UserTable] Script Date: 2022/6/29 20:08:23 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[UserTable](
[id] [varchar](36) NOT NULL,
[name] [varchar](256) NULL,
[code] [varchar](256) NULL,
[createtime] [datetime] NULL,
[lastmodifytime] [datetime] NULL,
PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
USE [Test]
GO
INSERT [dbo].[UserTable] ([id], [name], [code], [createtime], [lastmodifytime]) VALUES (N'5E4B68B0-71B8-43FB-B6B4-8E9D43A30589', N'test1', N'123456', CAST(N'2022-06-29T18:02:21.517' AS DateTime), CAST(N'2022-06-29T18:02:21.517' AS DateTime))
GO
由于Select語句在SQL Server的默認事務隔離級別(read commited)中執(zhí)行完成后就會釋放相關的鎖,而非等到事務結束,在這種情況下無法通過sp_lock或者sys.dm_tran_locks視圖觀察select語句執(zhí)行過程中鎖的執(zhí)行情況,因此比較方便的辦法是在查詢語句執(zhí)行之前調整當前會話的事務隔離級別為repeatable read,在這個隔離級別中select語句默認會在事故執(zhí)行完成后提交,比較方便分析。
在SQL Server Manager Studio的查詢窗口中執(zhí)行語句:
set transaction isolation level repeatable read
set statistics profile on
begin tran
select * from usertable where id='5E4B68B0-71B8-43FB-B6B4-8E9D43A30589'
在前面的事務目前是已經執(zhí)行未提交的狀態(tài),此時可以通過dm_tran_locks查詢到該語句目前持有的鎖:
select request_session_id,resource_type,request_status,request_mode,resource_description,
case resource_type
when 'Page' then OBJECT_NAME(p.object_id)
when 'object' then OBJECT_NAME(lock.resource_associated_entity_id)
when 'database' then (select name from master..SysDatabases where dbid=resource_database_id)
when 'key' then object_name(p.object_id)
end
as objectName from sys.dm_tran_locks lock
left join sys.partitions p
on p.hobt_id=lock.resource_associated_entity_id
order by lock.request_session_id
request_session_id |
resource_type |
request_status |
request_mode |
resource_description |
objectName |
62 |
DATABASE |
GRANT |
S |
|
Test |
62 |
PAGE |
GRANT |
IS |
0.236111111 |
UserTable |
62 |
OBJECT |
GRANT |
IS |
|
UserTable |
62 |
KEY |
GRANT |
S |
(0ee48b5e6942) |
UserTable |
查詢結果字段說明:
- request_session_id:會話編號
- resource_type:被鎖定的資源類型
- request_status:請求的狀態(tài)
- request_mode:鎖類型
- resource_description 資源描述情況
- objectName:對象名稱
目前select查詢持有的鎖:
- 通過目前的查詢結果可以看到在DATABASE上加了S鎖(數(shù)據庫名為Test);
- 在數(shù)據所屬的頁上增加了意向共享鎖;
- 表上增加了意向共享鎖;
- 數(shù)據行上增加了共享鎖;
目前的事務執(zhí)行過程中只對于匹配到的數(shù)據行進行了鎖定,如果插入刪除語句并未涉及到該數(shù)據行就不會受到影響,但是如果涉及到這行數(shù)據那肯定需要等S鎖釋放后才能進行。
SQL Server執(zhí)行insert時使用的鎖
首先在事務中執(zhí)行insert語句并且不提交(注意將上個章節(jié)中的事務提交):
begin tran
insert into UserTable (id,code,name,createtime,lastmodifytime)
values(newid(),'test2','測試用戶2',getdate(),getdate())
insert的時候默認會有事務,因此主動聲明一個事務并只執(zhí)行不提交就可以很容易地查到當前會話持有的鎖。
通過dm_tran_locks查詢到該語句目前持有的鎖:
request_session_id |
resource_type |
request_status |
request_mode |
resource_description |
objectName |
70 |
DATABASE |
GRANT |
S |
|
Test |
70 |
PAGE |
GRANT |
IX |
1:280 |
UserTable |
70 |
OBJECT |
GRANT |
IX |
|
UserTable |
70 |
KEY |
GRANT |
X |
(c75ad92ba798) |
UserTable |
該事務持有的鎖:
- 數(shù)據庫層面的共享鎖;
- 數(shù)據頁上的意向排他鎖;
- 數(shù)據表的意向排他鎖;
- 數(shù)據行的排他鎖;
結合上文中對于鎖類型的講解可以很容易理解數(shù)據庫增加這些鎖的用意。數(shù)據庫層面增加S鎖可以保護當前正在進行的事務的安全,同時針對發(fā)生數(shù)據變化的數(shù)據頁和數(shù)據表增加意向排他鎖可以防止其他事務對于數(shù)據庫和數(shù)據頁進行更高層的修改(比如架構級別或者DDL之類的事務),IX鎖對于IX和IS是可以并存的,因此可以最大限度上支持同一個區(qū)域內的其他修改和查詢事務。
SQL Server執(zhí)行update時使用的鎖
首先在數(shù)據庫中執(zhí)行update語句而不提交(注意將上個章節(jié)中的事務提交或者回滾):
begin tran
update UserTable set lastmodifytime=GETDATE() where id ='06757850-68D6-416C-B3D1-FD3B29BAD4BB'
通過dm_tran_locks查詢到該語句目前持有的鎖:
request_session_id |
resource_type |
request_status |
request_mode |
resource_description |
objectName |
52 |
DATABASE |
GRANT |
S |
|
Test |
52 |
PAGE |
GRANT |
IX |
1:280 |
UserTable |
52 |
OBJECT |
GRANT |
IX |
|
UserTable |
52 |
KEY |
GRANT |
X |
(ead909dc80bf) |
UserTable |
該事務持有的鎖:
- 數(shù)據庫層面的共享鎖;
- 數(shù)據頁上的意向排他鎖;
- 數(shù)據表上面的意向排他鎖;
- 數(shù)據行的排他鎖;
有了insert的經驗后,理解update語句使用的鎖難度就不大了。其與insert使用的鎖的類型基本一樣,由于本次是使用主鍵進行修改,數(shù)據庫可以直接定位到需要進行變更的數(shù)據行,因此只需要在對應的行上增加X鎖就可以滿足事務的需要。
日常使用的時候很少直接通過id更新數(shù)據,往往基于一些非聚集索引更新數(shù)據,在這種情況下數(shù)據庫對于鎖的使用會有什么不一樣呢?首先針對測試的數(shù)據表增加兩個索引:
create nonclustered index idx_UserTable_Name on UserTable(name)
create nonclustered index idx_UserTable_LastModifyTime on UserTable(lastmodifytime)
然后將update語句修改為根據name更新數(shù)據:
begin tran
set statistics profile on
update UserTable set lastmodifytime=GETDATE() where name like '%test%'
該語句對應的鎖的情況統(tǒng)計:
request_session_id |
resource_type |
request_status |
request_mode |
resource_description |
objectName |
52 |
DATABASE |
GRANT |
S |
|
Test |
52 |
PAGE |
GRANT |
IX |
1:280 |
UserTable |
52 |
PAGE |
GRANT |
IX |
1:368 |
UserTable |
52 |
KEY |
GRANT |
X |
(ba4eae1b81ad) |
UserTable |
52 |
KEY |
GRANT |
X |
(500c265deab6) |
UserTable |
52 |
KEY |
GRANT |
X |
(a1a185fdb4ae) |
UserTable |
52 |
OBJECT |
GRANT |
IX |
|
UserTable |
52 |
KEY |
GRANT |
X |
(ff4928fe375a) |
UserTable |
52 |
KEY |
GRANT |
X |
(0ee48b5e6942) |
UserTable |
可以發(fā)現(xiàn)通過非聚集索引更新數(shù)據的時候,數(shù)據庫需要檢查的內容明顯增加,并且增加IX鎖的數(shù)據也多了不少。只看這個表格可能不太好理解,這些key對應的X鎖為什么要增加,以及是使用的哪個索引呢?為了了解更多的信息,上文中查詢事務鎖的語句需要進行一些改動,增加對于索引的關聯(lián)查詢:
with indexs
as (
SELECT 索引名稱 = a.name ,
表名 = c.name ,
索引字段名 = d.name ,
a.indid
FROM sysindexes a
JOIN sysindexkeys b ON a.id = b.id
AND a.indid = b.indid
JOIN sysobjects c ON b.id = c.id
JOIN syscolumns d ON b.id = d.id
AND b.colid = d.colid
WHERE a.indid NOT IN ( 0, 255 )
AND c.name='UserTable' --查指定表
)
select request_session_id,resource_type,request_status,request_mode,resource_description,
case resource_type
when 'Page' then OBJECT_NAME(p.object_id)
when 'object' then OBJECT_NAME(lock.resource_associated_entity_id)
when 'database' then (select name from master..SysDatabases where dbid=resource_database_id)
when 'key' then object_name(p.object_id)
end
as objectName,index_id,i.索引名稱 from sys.dm_tran_locks lock
left join sys.partitions p on p.hobt_id=lock.resource_associated_entity_id
left join indexs i on i.indid=index_id
order by lock.request_session_id
通過關聯(lián)查詢索引信息,得到了更豐富的內容:
request_session_id |
resource_type |
request_status |
request_mode |
resource_description |
objectName |
index_id |
索引名稱 |
52 |
DATABASE |
GRANT |
S |
|
Test |
NULL |
NULL |
52 |
PAGE |
GRANT |
IX |
1:280 |
UserTable |
1 |
PK__Test1__3213E83F133024F3 |
52 |
PAGE |
GRANT |
IX |
1:368 |
UserTable |
4 |
idx_UserTable_LastModifyTime |
52 |
KEY |
GRANT |
X |
(ba4eae1b81ad) |
UserTable |
4 |
idx_UserTable_LastModifyTime |
52 |
KEY |
GRANT |
X |
(500c265deab6) |
UserTable |
4 |
idx_UserTable_LastModifyTime |
52 |
KEY |
GRANT |
X |
(a1a185fdb4ae) |
UserTable |
1 |
PK__Test1__3213E83F133024F3 |
52 |
OBJECT |
GRANT |
IX |
|
UserTable |
NULL |
NULL |
52 |
KEY |
GRANT |
X |
(ff4928fe375a) |
UserTable |
4 |
idx_UserTable_LastModifyTime |
52 |
KEY |
GRANT |
X |
(0ee48b5e6942) |
UserTable |
1 |
PK__Test1__3213E83F133024F3 |
52 |
KEY |
GRANT |
X |
(150ba0b85c41) |
UserTable |
4 |
idx_UserTable_LastModifyTime |
從上表中可以看出在更新數(shù)據的時候,由于涉及到多行的非聚集索引上面的數(shù)據,因此對于該索引涉及到的數(shù)據行都增加了X鎖,涉及到的數(shù)據頁也比之前更多了。類型為X鎖,同時索引名稱為PK__Test1__3213E83F133024F3的有兩行,因為本次事務匹配到了兩行數(shù)據;類型為X鎖,同時索引名稱為idx_UserTable_LastModifyTime的一共有四行。為什么是四行呢?因為有兩個舊的數(shù)據需要刪除,同時新增了兩個新的數(shù)據,所以是四行。其他的非聚集索引的數(shù)據并沒有修改,所以本次不需要申請X鎖。
總結
數(shù)據庫中的各種事務隔離級別都是通過對于不同鎖的綜合運用實現(xiàn)的。對于鎖的認識可以從兩個角度進行:鎖模式和鎖對象。哪怕是一個簡單的select語句都會有默認的某種鎖以保護數(shù)據的正確性。需要注意不同的數(shù)據組合情況、不同的事務隔離級別下SQL語句的執(zhí)行過程可能是不一樣的,因此其使用的鎖也會千變萬化,本文所列舉的只是一些很簡單的情況,但是規(guī)則類似,分析路徑也是基本一致的,有興趣的可以自己嘗試下日常工作中語句的執(zhí)行過程中使用的鎖,這對于理解數(shù)據庫工作原理,有針對性的對于SQL語句調優(yōu)都有一定幫助(注意不要在生產環(huán)境執(zhí)行這類分析)。
參考文檔
- SQL Server, Locks object
- 事務鎖定和行版本控制指南
- Microsoft SQL Server企業(yè)級平臺管理實踐