sql 鎖

SQL 名詞解釋

1. 事務
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.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
查出來的數據一樣,聚集索引就是這樣,記錄的物理存儲順序與
聚集索引的順序是一樣的.
 
看看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.8 隔離級別
前面已經說過了,有四種隔離級別:
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
1.9 顯式鎖
顯式鎖是在select、insert、delete、update語句裏指定鎖的類型,
如:
select * from authors with(RowLock) where au_id <='555-55-5555'
如果不用顯式鎖,那就是:
select * from authors where au_id <='555-55-5555'
如果不用顯式鎖,那採用那種鎖是由SQL SERVER內部來決定的, 
 
 
2. 數據庫設計
2.1 冗餘數據
冗餘數據就是重複的數據,冗餘數據出現的情況很多,這裏舉一種情況,
就是表之間的冗餘,假設我在northwind裏建立一個sales表,字段如:
timeId,date,year,month,custId,custName,productId,
QuanPerUnit,productName,regionID,regionName,storeId,
storeName,quantity,price,money,
現在我要查看2005年各商品的銷售情況,那可以用:
select productName,QuanPerUnit,sum(quantity),sum(money) 
from sales where year=2005 group by productId
如果我這樣建sales表:
timeId,custId,productId,storeId,regionID
quantity,price,money
那查詢是怎樣的?查詢就會鏈接幾個表,sql語句變得複雜一些,
速度也會慢得多,但是按前面的方式建立sales表,會忽略一個問題,
那就是數據冗餘的問題,從物理存儲上來說,productName已經存在了
products表裏,沒必要在sales表裏再存儲一次,這樣就可以,
而這裏又存儲了一次,那就要佔用更多的物理空間,
再看一個表內冗餘的情況,
如果sales表的字段是這樣的:
timeId,date,custId,proId,storeId,price,
northQuan,northMoney,eastQuan,eastMoney,
westQuan,westMoney,southQuan,southMoney
那如果要查2005年各區域產品的銷售情況,就可以用:
select
 (select productName from products where productId=proId) [產品]
 sum(northMoney) as [北部],
 sum(eastMoney) as [東部],
 sum(westMoney) as [西部],
 sum(southMoney) as [南部]
from sales where year(date)=2005
group by proId
上面的表的後面8個字段都是冗餘的,
只要用regionId這個字段就足夠了,
表內冗餘的情況還有一些,理解冗餘應從反面去理解,
就是如何去避免冗餘,要避免冗餘,那凡是可以通過直接或間接
方式得出來的數據,就不要在表裏保存,比如通過錶鏈接得到或
通過計算得到等等的數據,就沒必要在表裏再保存一次了
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章