查詢未提交事務個數

 

查詢未提交事務個數

select   @@TRANCOUNT

前受MSSQL系統管理指南的影響,以爲鎖表就是status列是wait狀態的,
其實如果是未提交的事務,那該進程過一段時間就不會是wait了,
不過即使不是wait,用sp_lock還是可以看到一些信息,
找到未提交的事務的進程SPID,執行下面語句
dbcc inputbuffer(SPID),可以跟蹤該進程正在執行的SQL語句,
一般嵌套事務使用不好最容易導致未提交的事務,
還要結合sp_who2、master..sysprocesses 等來判斷,
綜合判斷才能知道問題出在哪裏,
我以前就碰到過幾次這樣的問題,系統運行變的很慢很慢
執行 sp_lock,並沒有status = wait 的列,
不過有幾個活動的spid,再執行sp_who、sp_who2、查閱sysprocesses,dbcc inputbuffer
基本能確定是執行什麼語句導致鎖住了數據,
並查看出問題的客戶機(sysprocesses裏有),
然後再去問那臺機的使用者,最近執行了什麼操作,他回答操作了XXX,
然後自己建測試庫,執行同樣的操作,跟蹤,才找到問題的癥結,

很鬱悶的,

有A和B兩個存儲過程,B存儲過程可以單獨調用,B有事務,
A需要調用B,A也有事物,如:
Procedure A
    begin tran ta
    exec b
    commit tran ta
end

procedure B
   begin tran tb
   delete stb
   if(@@rowcount=0) begin
      rollback tran tb
      return
   end
   commit tran tb
end

單獨調用B時,正常,
A調用B時,如果執行 rollback tran tb 語句時就出錯,
提示“無法回滾 tb。沒有找到任何該名稱的事務或保存點”

嵌套事務裏,回滾存在問題,

     if(@@trancount = 1 ) begin    --爲1表示是最外層事務,大於1時表示是嵌套事務,
         rollback tran tb
     end else begin
         commit tran tb   --這裏即使提、提交,外層事務回滾一樣會把提交的數據給回滾的
     end

1.1 行級鎖

行級鎖是針對行來鎖定的,比如在事務裏,進程A執行了一條update語句: 
update student set name='xx' where id=13 
則行級鎖會鎖住student表裏id=13的記錄,不讓別的進程對它操作, 
只有等事務完成後才解除鎖,舉個例子,以 SQL SERVER爲例, 
同時打開兩個查詢分析器,在第一個查詢分析器裏寫:

use northwind
select * from suppliers 
begin transaction    update suppliers set CompanyName='xx' where SupplierID=3    waitfor delay '00:00:20' 
commit transaction

在第二個查詢分析器裏寫:

select * from suppliers

然後先運行第一個查詢分析器裏的代碼,再運行第二個查詢分析器裏的 
代碼,可以看到第一個查詢分析器一直運行,運行了大概20秒後執行 
完畢,第二個查詢分析器也一樣,運行了大概20秒才停止, 
這說明執行 select * from suppliers 時在等待,如果不運行第一個 
查詢分析器裏的代碼,直接運行第二個查詢分析器裏的代碼,那幾乎不 
用等待就可以看到結果了; 
修改第二個查詢分析器的代碼爲: 
select * from suppliers where SupplierID<>3 
然後先運行第1個查詢分析器的代碼,再運行第二個查詢分析器的代碼, 
可以看到第二個查詢分析器一運行馬上就出結果了,沒有等待, 
再修改代碼爲: 
select * from suppliers where SupplierID=3 
重複前面的操作,可以看到需要等待,等待約20秒後纔看到結果 
這很明顯的告訴我們,行級鎖會鎖住修改的行,讓別的進程無法操作 
那些行,只有事務完成後別的進程纔可以操作,而沒有修改的行,別的 
進程就可以任意操作,不會有限制

1. 事務

1.2 頁級鎖(1)

先理解頁這個概念,在SQL SERVER裏建表時,如果字段大小比較大時, 
往往會有提示: 表中允許的最大行大小 8060 
比如在SQL SERVER2000 裏新建一個表:

create table Test(Fld1 char(5000),Fld2 char(5000))

會提示建立失敗,原因如: 
“創建表 'Test' 失敗,因爲行大小將爲 10021(包括內部開銷),而該值超過了表中允許的最大行大小 8060。”

爲什麼它會限制行大小爲8060呢?因爲在 SQLSERVER2000裏,一頁的 
大小爲8K,這8K裏包括96字節的頁頭、36字節的其它信息、8060字節的 
數據區,而數據就存儲在8060字節的數據區裏, 
一頁能存儲多少行呢?這要看行的大小了,比如如果行大小爲2000, 
則一頁能存儲4行,注意行大小不包括文本和圖象字段, 
比如數據庫northwind的customers表的行大小爲 298, 
則一頁可以存儲 27 行

看看行大小計算的問題:

use northwind 
alter table customers add xx char(8000)

運行結果有警告: 
----------------------------------------- 
警告: 已創建表 'customers',但其最大行大小(8578)超過了每行的最大字節數(8060)。如果結果行長度超過 8060 字節,則此表中行的 
INSERT 或 UPDATE 將失敗 
----------------------------------------- 
它提示行大小爲8578,則修改前的customers的行大小爲578, 
可爲什麼我將各個字段的大小加起來才268呢, 
這有兩個原因,一方面,數據庫用兩個字節存儲一個nvarchar類型的字符 
nchar也一樣,而customers的字段類型爲nchar和nvarchar,所以實際大小爲 268*2=536 ,那還有42呢?42是表的其它開銷。

從行與頁的關係可以看出,行的大小越小,則一頁能存儲的行數越多, 
數據庫查詢時,從一頁讀到另一頁,比只讀一頁的記錄要慢得多, 
所以要減少跨頁讀取的次數,

比較下面的兩個語句:

create table x1(a char(5000),b char(5000)) 
create table x2(a varchar(5000),b char(5000))

運行結果爲: 
------------------------------------ 
服務器: 消息 1701,級別 16,狀態 2,行 2 
創建表 'x1' 失敗,因爲行大小將爲 10021(包括內部開銷),而該值超過了表中允許的最大行大小 8060。 
警告: 已創建表 'x2',但其最大行大小(10023)超過了每行的最大字節數(8060)。如果結果行長度超過 8060 字節,則此表中行的 INSERT 或 
UPDATE 將失敗。 
------------------------------------ 
x1創建失敗,x2創建成功,但有警告,爲什麼呢? 
這要比較char和varchar的區別了,當創建x1時,最大行大小10023就是 
實際的行大小,因爲char是定長的,大小總是10023,而x2不同, 
varchar是變長的,雖然最大行大小是10023,而實際行大小卻不一定的, 
實際行大小隨字段a的值的大小的變化而變化, 
所以,每頁能存儲的行數,如果是定長的,那在建表時就可以確定了, 
如果是變長的,那要根據表中的數據來確定,當然,SQL SERVER存儲記錄 
時,對於頁的選擇還會考慮一些問題,也並不完全是這樣    看看Northwind數據庫的Customers表吧, 
Customers的主鍵字段爲CustomerID,主鍵是聚集索引的, 
主鍵的順序代表了行的實際存儲順序, 
比如你往Customers裏插入一條記錄: 
insert into Customers(CustomerId,CompanyName) values('cvcvc','ff')

然後用select * from customers查看數據, 
可以看到新插入的記錄自動排在了CustomerId等於CONSH的後面, 
看起來就和 select * from customers order by customerId 
查出來的數據一樣,聚集索引就是這樣,記錄的物理存儲順序與 
聚集索引的順序是一樣的,

1.2 頁級鎖(2)

看看Customers表,打開三個查詢分析器,在第一個表寫: 
begin transaction 
update customers with(PagLock) set Address=Address 
where customerId='ALFKI' 
waitfor delay '00:00:30' 
commit transaction 
在第二個查詢分析器裏寫: 
select * from customers where customerId='GREAL' 
在第三個查詢分析器裏寫: 
select * from customers where customerId='GROSR' 
先運行第一個查詢分析器,然後運行第二個,再運行第三個, 
可以看到,第一和第二個查詢分析器等待執行了20秒, 
而第三個查詢分析器沒有等待立即就顯示運行結果了, 
我更新的是'ALFKI',因爲是頁鎖,所以它鎖住了一頁的數據, 
從'ALFKI'到'GREAL'的行都鎖住了,這也說明, 
'ALFKI'到'GREAL'之間的共34行都是屬於同一頁的, 
你可以將第一個查詢分析器的'ALFKI'換成'DRACD',可以看到運行 
結果是一樣的,如果換成'HANAR',那結果就變了, 
變成第一個和第三個查詢分析器在等待,而第二個查詢分析器不用等待, 
因爲'HANAR'和'GROSR'屬於同一頁,而'GREAL'在其它的頁, 
頁鎖概念是比較簡單的,但頁的概念卻比較複雜, 
頁是在SQLSERVER的內部管理的,用戶看不到,頁比較抽象, 
對於變長的數據類型,頁的分配是隨數據的變化而變化的, 
請參考數據庫相關的資料。

1.3 表鎖

在第一個查詢分析器寫: 
begin transaction tran1 
update customers with(TabLock) set City=City 
where CustomerId='ALFKI' 
waitfor delay '00:00:20' 
commit transaction tran1

在第二個查詢分析器寫: 
select * from customers where customerId='WOLZA'

先運行第一個查詢分析器,再運行第二個,兩個查詢分析器都在等待. 
注意customerId='WOLZA'是表的最後一條記錄

1.4 阻塞

前面的例子裏一個事務未提交,導致別的事務必須等待,這就是阻塞, 
查看阻塞可以用sp_lock,打開三個查詢分析器, 
第一個寫: 
begin transaction tran1 
update products set productName=productName+'A' 
where ProductId=1 
waitfor delay '00:00:30' 
commit transaction tran1 
第二個寫: 
select * from products 
第三個寫: 
sp_lock 
依次運行第一個、第二個、第三個, 
然後查看第三個分析器,看看Status列, 
看是否有Status='Wait'的行,比如我這裏查看有這麼一行:

53 6 117575457 1 KEY (010086470766) S WAIT

其中ObjId=117575457

然後運行: 
use northwind 
select object_name(117575457) 
可以看到對應的表爲 Products

1.5 死鎖

同時打開兩個查詢分析器, 
第一個寫: 
begin transaction tran2 
update products set productName=productName+'A' 
where ProductId=2 
waitfor delay '00:00:10' 
update products set productName=productName+'A' 
where ProductId=1 
commit transaction tran2

第二個寫: 
begin transaction tran1 
update products set productName=productName+'A' 
where ProductId=1 
waitfor delay '00:00:10' 
update products set productName=productName+'A' 
where ProductId=2 
commit transaction tran1

先運行第一個,再運行第二個 
然後等待它們執行,等待大概十多秒, 
檢查運行結果,可以看到其中一個出錯,錯誤提示如:

服務器: 消息 1205,級別 13,狀態 50,行 1

在查詢分析器裏按F1打開幫助,在幫助裏選擇索引選項卡, 
輸入 1205 ,你仔細查看幫助文檔是如何描述 1205 錯誤的,

爲什麼會死鎖呢?看看執行過程,爲了簡單,我將productId簡寫爲id 
先是分析器1更新id=2的記錄,並鎖住id=2的記錄,那別的進程都無法 
操作id=2的記錄, 
然後分析器2更新id=1的記錄,並鎖住id=1的記錄,同樣別的進程無法 
操作id=1的記錄, 
然後分析器1更新id=1的記錄,因爲id=1的記錄被分析器2鎖住了, 
所以必須等待,分析器1被阻塞 
同樣分析器2更新id=2的記錄,因爲id=2的記錄被分析器1鎖住了, 
所以也要等待,分析器2被阻塞 
兩個分析器都要等待對方,所以就出現死鎖,哪個都不能執行,

當然,SQLSERVER2000爲了解決死鎖問題,它會幹掉其中一個進程 
來結束死鎖。

1.6 佔用讀

佔用讀指可以讀別的進程未提交的數據, 
打開兩個查詢分析器,第一個寫: 
begin transaction tran1 
update products set productName=productName+'C' 
where ProductId=1 
waitfor delay '00:00:15' 
commit transaction tran1

第二個寫: 
set transaction isolation level read uncommitted 
select * from products where ProductId=1

依次運行第一個和第二個, 
可以看到第一個在等待,而第二個不用等待, 
因爲我在第二個裏設置了隔離級別爲read uncommitted, 
就是允許讀別的事務未提交的數據, 
你看看第二個的運行結果,找到products列,看到products列已經修改了 
如果你修改第二個查詢分析器代碼爲: 
set transaction isolation level read committed 
select * from products where ProductId=1

同樣運行,那第二個也要等待了,因爲隔離級別是read committed, 
只能讀提交後的數據,不能讀未提交的修改,這樣就防止了 
佔用讀,SQLSERVER2000裏默認是read committed

 

說明,佔用讀也叫髒讀,髒讀就是修改了但沒提交的數據, 
在文本編輯器裏也有髒讀的概念,就是修改了但未保存的數據

1.7 不可重複讀

事務裏執行兩次相同的查詢時,查詢出來的結果不相同, 
說明是不可重複讀,打開兩個查詢分析器, 
第一個寫: 
use northwind 
set transaction isolation level read committed 
begin transaction tran1 
select * from region where regionId=3 
waitfor delay '00:00:10' 
select * from region where regionId=3 
commit transaction tran1

第二個寫: 
use northwind 
update region set regionDescription='xx' where regionId=3

依次運行第一個和第二個分析器,第一個分析器等待10秒,第二個 
不用等待立即得到結果,第一個分析器運行結果爲: 
3 Northern 
3 xx

兩次讀得的值不相同, 
修改第一個查詢分析器代碼爲: 
use northwind 
set transaction isolation level repeatable read 
begin transaction tran1 
select * from region where regionId=3 
waitfor delay '00:00:10' 
select * from region where regionId=3 
commit transaction tran1

第二個修改爲: 
use northwind 
update region set regionDescription='yy' where regionId=3

同樣依次運行第一個和第二個,看到第一個在等待, 
第二個也在等待,第一個分析器運行結果爲: 
3 xx 
3 xx

看看兩次的區別,第一次我設置隔離級別爲read committed, 
第二次我設置爲repeatable read, 
repeatable read 會鎖住讀的數據, 
read committed 會鎖住修改的數據, 
repeatable read會鎖住insert、update、delete、select操作的數據 
read committed只鎖insert 、update、delete, 不鎖select查詢的數據

1.8 幻像讀

打開兩個查詢分析器,第一個寫: 
use northwind 
set transaction isolation level repeatable read 
begin transaction tran1 
select * from region 
waitfor delay '00:00:10' 
select * from region 
commit transaction tran1

第二個寫: 
use northwind 
insert into region values(5,'xx')

依次運行第一個和第二個分析器,第一個分析器等待10秒,第二個 
不用等待立即得到結果,第一個分析器運行結果爲: 
1 Eastern 
2 Western 
3 Northern 
4 Southern

1 Eastern 
2 Western 
3 Northern 
4 Southern 
5 xx

比較兩次查詢的結果,第二次查詢多了一行, 
修改第一個分析器的代碼爲: 
use northwind 
set transaction isolation level serializable 
begin transaction tran1 
select * from region 
waitfor delay '00:00:10' 
select * from region 
commit transaction tran1

修改第二個分析器代碼爲: 
use northwind 
insert into region values(6,'yy')

再依次運行第一個和第二個分析器, 
可以看到兩個分析器都要等待,第一個的運行結果是: 
兩次查詢返回的行數是相同的。

理解佔用讀、不可重複讀、幻像讀要從數據庫如何操作來避免他們上 
來理解,如果只從概念上去理解,概念往往很抽象,比較晦澀難懂, 
而且概念往往只說到其中一個方面,應該弄清楚各種級別的瑣是如何 
避免出現佔用讀、不可重複讀、幻像讀的。 
read uncommitted不設置鎖, 
read commmitted會鎖住update、insert、delete 
repeatable read會鎖住update、insert、delete、select 
seriablizable會鎖住update、insert、delete、select

repeatable read和seriablizable的區別在於: 
repeatable read鎖住時別的事務不能update、delete鎖住的數據, 
但別的事務能夠插入, 
seriablizable鎖住時別的事務不能update、delete、insert     說明: 
repeatable read 和 seriablizable 對 select 的鎖定採用範圍的方式 
要鎖哪些行,主要是受where語句的限制,另外還受行鎖、頁鎖、表鎖方式 
的限制,對於update、insert、delete的鎖定範圍比較明確, 
repeatable read隔離級別對select的鎖定也比較明確, 
而seriablizable對select的鎖定,當別的事務insert時, 
哪些時候不能插入呢?這個範圍如何確定? 
因爲鎖的概念往往是針對已有的數據,而insert插入的數據是原來表裏 
沒有的,原來表裏沒有,那又如何鎖定呢? 
比如: 
set transaction isolation level seriablizable 
select * from region 
則鎖住表的所有行,比如開始有四行,則鎖住四行, 
那insert一行呢,這裏有個範圍,那就是select的範圍, 
它會判斷insert的行是否在鎖定的範圍之內,比如: 
use northwind 
set transaction isolation level serializable 
begin transaction tran1 
select * from region where regionId>6 
waitfor delay '00:00:10' 
commit transaction tran1

如果別的事務插入記錄: 
insert into region values(5,'yy')

因爲插入的記錄regionId=5,而6<5,不滿足鎖定的條件, 
所以該插入是允許的,如果別的事務插入記錄: 
insert into region values(7,'yy') 
因爲7>6,滿足條件,所以插入被阻塞,只能等待鎖釋放才能插入 
不過,到底鎖定哪些記錄,這比較難說,鎖做爲SQLSERVER的內部管理, 
到底是怎麼樣的不怎麼清楚,我試過有些情況不滿足的條件的記錄也 
被阻塞,不過有點是清楚的,那就是滿足條件的一定被阻塞

1.9 隔離級別

前面已經說過了,有四種隔離級別: 
read uncommitted 
read committed 
repeatable read 
serializable

read committed是默認的隔離級別, 
隔離級別對單個用戶有用,比如你設置了隔離級別爲serializable, 
那隻對你自己有用,對別的用戶不起作用,設置了隔離級別後, 
那一直有效,直到用戶退出爲止, 
鎖的作用主要是用來保證數據一致性的, 
read uncommitted不會在被讀的數據上放置鎖,所以它執行 
的速度是最快的,也不會造成阻塞和死鎖,但因爲數據一致 
性問題,所以往往不採用,當然可以通過別的技術比如增加 
rowverision列等來保證數據一致性,但對於複雜的操作,還 
是選擇事務比較安全,對於事務我經歷過一些教訓, 
比如一次我在VB裏保存數據,大致如:

begin transaction 
declare @id as int 
insert into A(...) values(...) 
select @id=max(id) from t1 
insert into B(AId...) values(@id...) 
commit transation

其中A表的id字段是自動編號的,我先在A表插入一條記錄, 
再將A表剛插入的記錄的Id插入到B表,必須保證@id是前面insert 
生成的那個id, 
但測試時,因爲客戶有很多電腦同時錄,所以導致一些id不一致的情況, 
爲什麼會不一致呢?我不是已經加了事務了嗎? 
做一個例子,同樣打開兩個查詢分析器,第一個寫:

use northwind 
set transaction isolation level serializable 
begin transaction tran1 
insert into region values(10,'aa') 
waitfor delay '00:00:10' 
select max(regionId) from region 
commit transaction tran1

第二個寫: 
insert into region values(11,'aa')

依次執行第一個和第二個分析器, 
本來我希望第一個分析器裏查詢出來的是10,可結果卻是11 
在這裏,唯一的方法是指定表鎖,如: 
insert into region with(TabLock) values(10,'aa')

只有指定表鎖,讓別的事務無法操作它,才能保證數據一致, 
當然,如果是自動編號,那可以用 @@identity 來獲取剛生成的Id號, 
比如: 
insert into orders(CustomerId) values('ALFKI') 
print @@identity

這個技巧在別的數據庫驅動程序裏可能無效,事務纔是普遍支持的    補充(表來源: SQL SERVER7.0 系統管理指南)

隔離級別    阻塞風險 防止佔用讀 防止不可重複讀 防止幻像讀 
--------------------------------------------------------------- 
read uncommitted 最低   NO      NO       NO 
read committed  較低   Yes      NO       NO 
repeatable read 較高   Yes      Yes       NO 
serializable   最高   Yes      Yes       Yes
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章