關於鎖,有很多可以學習的,比如,鎖的粒度,鎖的模式,鎖的相容性,以及隔離等級等等。
但是,具體到運行一個語句時,是怎麼去擷取到相應的鎖的呢,比如,一個select語句,那麼需要加S鎖,可能是對記錄,也可能是對頁,也可能是對錶,可能通過索引去找到記錄,找到記錄後,是一次擷取所有的鎖,還是一部分一部分的擷取呢?
比如下面實驗中,正好是一條記錄佔用1個資料頁,一共查詢32768條資料,那麼到底是對32768個頁都加上S鎖,還是先對一些頁加鎖,查詢出一部分資料,然後再對剩下的頁加鎖,再查詢出一部分資料。
又比如,現在有2個會話,會話1運行select 語句來查詢資料,假設查詢資料需要大概耗時18秒,而會話2是一個update語句來更新同樣的資料,大概需要耗時15秒,會話1先運行,會話2在一秒後運行,那麼哪個語句會先運行完呢?
一般的想法,應該是會話1先返回,因為會話1的select語句,會對資料頁加上共用鎖定的,既然加上了共用鎖定,那麼會話2是後啟動並執行,要對頁加獨佔的X鎖,肯定是等待的,但實際情況卻完全不是這樣。如果是還有10個select語句的會話,又會是那個先運行完呢?從實際執行的情況來看,是update語句先執行完的。
言歸正傳,實驗代碼
1、建表,插入資料,建索引,這段代碼中批處理的次數是18次,也就是go 18,你可以根據實際情況修改,只要保證足夠的資料量,使select語句運行需要20秒左右就可以
if OBJECT_ID('test') is not null drop table testgocreate table test(id int,v varchar(8000))insert into testselect 1,replicate('a',8000) union allselect 2,replicate('o',8000) union allselect 3,replicate('c',8000) union allselect 4,replicate('d',8000) union allselect 5,replicate('e',8000) union allselect 6,replicate('f',8000) union allselect 7,replicate('g',8000) union allselect 8,replicate('h',8000) union allselect 9,replicate('i',8000) union allselect 10,replicate('j',8000) union allselect 11,replicate('k',8000) union allselect 12,replicate('l',8000)goinsert into testselect *from testgo 18create index idx_test_id on test(id)
2、建立一個temp_lock表,用來儲存sys.dm_tran_locks的資訊,這個主要是用來監控語句執行時詳細的鎖資訊
if OBJECT_ID('temp_lock') is not null drop table temp_lockgoselect 0 id,* into temp_lockfrom sys.dm_tran_lockswhere 1 = 2
接下來,需要建立3個查詢,依次執行下面3個查詢,也就是先執行會話1的代碼,然後執行會話2的代碼,然後執行會話3的代碼,這種中間會有正常的時間間隔(不用同時執行)。
3、會話1是監控代碼,注意這個代碼是死迴圈,所以需要在會話2和會話3的代碼,執行完後,取消執行查詢,否則會一直運行下去
declare @i intset @i = 1while 1=1begin insert into temp_lock select @i,* from sys.dm_tran_locks set @i = @i +1 end
4、查詢2是select語句
select GETDATE()begin transelect id, %%lockres%%, --檔案id:頁id:記錄id vfrom testwhere id =2commit transelect GETDATE()
5、查詢3是update語句
select GETDATE()update testset v =replicate('x',8000)where id = 2 select GETDATE()
6、運行結果
select 語句的已耗用時間是 2014-03-05 14:45:10.510 - 2014-03-05 14:45:27.107
update語句的已耗用時間是 2014-03-05 14:45:08.810 - 2014-03-05 14:45:24.247
由於監控到的資訊太多,這裡只是選擇了少量的資料,注意,在圖的右邊,有個欄位request_session_id 是請求會話id,55是update語句所在的會話,而57是select語句所在的會話,顯然,這行記錄上有一個X鎖,而select語句的會話被update的會話所阻塞,所以request_status 顯示為wait,也就是在等待擷取S鎖
7、那麼從上面的圖中能說明什麼呢?
第一,update語句雖然晚2秒才運行,但是卻更早完成了。
第二,select語句中返回的結果,既包含了沒有修改的原來的資料,也包含了update語句更新後的資料。
第三,update語句阻塞了select語句。
之前只知道,在預設的讀已提交隔離等級下,在一個事務中,select語句會在語句運行結束後,才會釋放S鎖,但是通過上面的實驗,應該是只要select語句在執行時,把一部分結果發送到用戶端,也就是我們在ssms的結果集視窗看到結果的時候,這個S鎖就會釋放,而不是等select語句整個運行完後,才釋放的,否則update語句會一直被select語句阻塞在那裡,設定出現死結(也就是,select語句擷取讀取了一部分資料,輸出,但是仍然持有S鎖,而update語句也更新了一部分資料,而持有對這些資料的X鎖,而select語句要等待已被X鎖鎖定的資料,而update語句要等待已被select語句鎖定的資料)。
既然是這樣,那麼select語句在運行時,肯定也是先擷取一部分資料的S鎖,然後輸出,然後接下來再擷取下一部分的資料的S鎖,然後再輸出,這樣就導致了可能被update語句阻塞,因為這個時候update語句已經對同一批資料都加上了X鎖了,所以就導致了上面的第三個阻塞的情況。
既然是被阻塞了,那麼select語句就會比update語句啟動並執行慢了,而update語句的X鎖,會一直保持到事務提交或復原。同樣的,為什麼select語句讀取到了一部分update修改後xxxxxxxx...資料呢,而不全是ooooooooo....資料呢?
因為既然select語句被update語句阻塞住了,那麼只有等update語句提交以後,才能讀取到,所以讀取了update語句提交之後的資料,也就是xxxxxxxx...
8、說了這麼多,難道這個是SQL Server的bug?
我認為不是,因為在read committed隔離等級下,只能保證select語句讀取到已提交的資料,而不能保證在一個事務中的一個select語句(注意不是一個事務中的前後2個select語句),只能讀取某一個時間點的資料,也就是不能保證這個select語句,讀取到的是開始運行時這個 2014-03-05 14:45:10.510 時間點的資料,因為在這個時間點後,資料被修改且提交了,所以他也會讀取這個已經修改了的資料,所以這個不是bug。