MYSQL select時鎖定記錄問題


在使用SQL時,大都會遇到這樣的問題,你Update一條記錄時,需要通過Select來檢索出其值或條件,然后在通過這個值來執行修改操作。

但當以上操作放到多線程中並發處理時會出現問題:某線程select了一條記錄但還沒來得及update時,另一個線程仍然可能會進來select到同一條記錄。

 

一般解決辦法就是使用鎖和事物的聯合機制

如:

1. 把select放在事務中否則select完成, 鎖就釋放了
2. 要阻止另一個select, 則要手工加鎖select 默認是共享鎖, select之間的共享鎖是不沖突的, 所以, 如果只是共享鎖, 即使鎖沒有釋放, 另一個select一樣可以下共享鎖, 從而select出數據

BEGIN TRAN
SELECT * FROM table WITH(TABLOCKX)

或者 SELECT * FROM table WITH(UPDLOCK, READPAST) 具體情況而定。

UPDATE ....
COMMIT TRAN

 

鎖描述:
HOLDLOCK:將共享鎖保留到事務完成,而不是在相應的表、行或數據頁不再需要時就立即釋放鎖。HOLDLOCK 等同於 SERIALIZABLE。 
NOLOCK 不要發出共享鎖,並且不要提供排它鎖。當此選項生效時,可能會讀取未提交的事務或一組在讀取中間回滾的頁面。有可能發生臟讀。僅應用於 SELECT 語句。 
PAGLOCK:在通常使用單個表鎖的地方采用頁鎖。 
READCOMMITTED:用與運行在提交讀隔離級別的事務相同的鎖語義執行掃描。默認情況下,SQL Server 2000 在此隔離級別上操作。 
READPAST:跳過鎖定行。此選項導致事務跳過由其它事務鎖定的行(這些行平常會顯示在結果集內),而不是阻塞該事務,使其等待其它事務釋放在這些行上的鎖。 READPAST 鎖提示僅適用於運行在提交讀隔離級別的事務,並且只在行級鎖之后讀取。僅適用於 SELECT 語句。 
READUNCOMMITTED:等同於 NOLOCK。 
REPEATABLEREAD:用與運行在可重復讀隔離級別的事務相同的鎖語義執行掃描。 
ROWLOCK:使用行級鎖,而不使用粒度更粗的頁級鎖和表級鎖。 
SERIALIZABLE:用與運行在可串行讀隔離級別的事務相同的鎖語義執行掃描。等同於 HOLDLOCK。 
TABLOCK:使用表鎖代替粒度更細的行級鎖或頁級鎖。在語句結束前,SQL Server 一直持有該鎖。但是,如果同時指定 HOLDLOCK,那么在事務結束之前,鎖將被一直持有。 
TABLOCKX 使用表的排它鎖。該鎖可以防止其它事務讀取或更新表,並在語句或事務結束前一直持有。 
UPDLOCK:讀取表時使用更新鎖,而不使用共享鎖,並將鎖一直保留到語句或事務的結束。UPDLOCK:的優點是允許您讀取數據(不阻塞其它事務)並在以后更新數據,同時確保自從上次讀取數據后數據沒有被更改。 
XLOCK:使用排它鎖並一直保持到由語句處理的所有數據上的事務結束時。可以使用 PAGLOCK 或 TABLOCK 指定該鎖,這種情況下排它鎖適用於適當級別的粒度。

 

 

SQL2008 行鎖使用RowLock

一直有個疑問,使用 select * from dbo.A with(RowLock) WHRE a=1 這樣的語句,系統是什么時候釋放行鎖呢??

 

經過官方文檔考證后,原來 RowLock在不使用組合的情況下是沒有任何意義的,所謂“解鈴還須系鈴人~”

 

With(RowLock,UpdLock) 這樣的組合才成立,查詢出來的數據使用RowLock來鎖定,當數據被Update的時候,鎖將被釋放

 

 

sqlserver鎖定數據庫中的一行記錄

跟我對鎖的疑惑差不多,就是,如何鎖定一條記錄,防止並發
說是存儲過程插入了兩條相同的記錄,
存儲過程的腳本如下:

SQL code
 
?
1
2
3
4
5
6
7
8
9
10
ALTER  PROC [dbo].[ Insert ]
     @Tid  Int
AS
BEGIN
  
     IF  NOT  EXISTS( SELECT  FROM  Table  WHERE  TId = @Tid)
     BEGIN
         INSERT  INTO  Table  (INSERTDATE,TID     )  VALUES  (GETDATE(),  @Tid);
          END
END



看了一下他的存儲過程,也做了是否存在的判斷,但這種判斷在並發執行下是遠遠不夠的,因為可能有多個回話判斷到某一條記錄不存在,然后同時插入,所以,出現帖子中描述的問題就不足為奇了,這個測試起來也很簡單,接觸sqlquerystress這個工具,開啟多個線程,每個線程多次循環插入
首先,建立一張表,類似這個一個存儲過程,表上建立非唯一的索引

SQL code
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if exists( select  from  sys.objects  where  type= 'U'  and  name  = 'testlock1'
drop  table  testlock1
 
--建表
create  table  testlock1
(
     id  int ,
     Createdate datetime,
)
 
--建立索引
create   index  index_1  on  testlock1(id)
 
--建立存儲過程插入數據
create  proc ups_TestLock
@i  int
as
begin
     begin  try
         begin  tran
             if  not  exists( select  from  where  id=@i )
             begin 
                 insert  into  testlock1  values  (@i,GETDATE());
             end
         commit
     end  try
     begin  catch
         rollback
     end  catch
end




關於並發測試,我們借助於sqlquerystress這個工具,下面會有截圖,測試腳本如下

SQL code
 
?
1
2
3
declare  @i  int
set  @i= cast (  rand()*100000  as  int ) --生成一個100000以內的隨機數
exec  test_p @i   



在sqlquerystress這個工具中,開啟30個線程,每個現成循環插入2000條數據
如截圖


好了,記錄插入完成(本文不是性能測試,不用太關注時間指標),有沒有重復的數據呢?
直接上圖,有圖有真相,重復記錄還真不少


原因在哪里?上面說了,因為可能有多個回話判斷到某一條記錄不存在,然后同時插入,這樣就造成了插入重復數據的情況
那么,改如何做判斷才能防止類似的並發造成的問題呢?
於是我想到鎖,其實想到鎖的時候我心里是沒譜的,一直沒太弄明白那些顯式的鎖提示,到底行不行,有沒有問題,於是就測試吧
於是我把存儲過程改成這樣

SQL code
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
alter  proc ups_TestLock
@i  int
as
begin
     begin  try
         begin  tran
             if  not  exists( select  from  with (xlock,rowlock)   where  id=@i ) --注意這里加上<span style="font-family: Arial, Helvetica, sans-serif;">xlock,rowlock,行級排它鎖</span>
             begin 
                 insert  into  testlock1  values  (@i,GETDATE());
             end
         commit
     end  try
     begin  catch
         rollback
     end  catch
end


用truncate table testlock1 清空剛才的測試表,繼續上的測試
令人不解的是這次還有重復記錄,雖然比一開始少了一些,但是鎖定的問題歸總還是沒有解決

想來想想去不知道問題出在哪里,用sp_lock @@spid查看回話的鎖信息的時候,確實有一個key級的排它鎖,但是為什么沒有鎖定記錄呢?
如圖

后來上網查,有人說要建立唯一索引,才能鎖定一行記錄,將索引改成唯一索引后
如下腳本

SQL code
 
?
1
2
drop  index  index_1  on  testlock1
create  unique  index  index_1  on  testlock1(id)


在測試,發現確實沒有重復記錄了,想想是不是巧合呢?又反反復復測了即便,確實沒有重復的,證明在查詢條件上建立唯一索引后,然后加上xlock,rowlock后
確實“鎖住”記錄了,解決了並發問題

事情到這里還沒有結束,為什么呢?下班時候,在公交車上還在想這個問題……
后來想想,非唯一索引無法“鎖定”記錄,出現重復的問題,唯一索引解決了並發,
問題肯定還是並發時候,因為是多線程並行插入的,會不會是不同線程同時插入的,
就是說:A,B兩個線程同時插入一條id為12345的數據,他們在插入之前判斷的時候,數據庫那個時刻中,確實沒有id為12345的數據
所以就同時插入了,那么根據這里的推理,重復數據肯定是不同回話插入的,
想起來真令人興奮,測測看吧
於是我將表結構修改為如下,增加一個插入的回話ID列

SQL code
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if exists( select  from  sys.objects  where  type= 'U'  and  name  = 'testlock1'
drop  table  testlock1
 
--建表
create  table  testlock1
(
     id  int ,
     Createdate datetime,
     SessionID  varchar (50)
)
 
--建立索引
create  index  index_1  on  testlock1(id)
 
 
alter  proc ups_TestLock
@i  int
as
begin
     begin  try
         begin  tran
             if  not  exists( select  from  testlock1  with (xlock,rowlock)  where  id=@i )
             begin 
                 insert  into  testlock1  values  (@i,GETDATE(),@@spid); --這里插入一列回話ID
             end
         commit
     end  try
     begin  catch
         rollback
     end  catch
end



繼續用sqlquerystress測試,線程還是30個,每個線程循環插入2000次

再次用該腳本查詢

SQL code
 
?
1
2
3
select  COUNT (1),id,Createdate  from  testlock1
group  by  id,Createdate
having ( COUNT (1))>1




有兩條重復的,那么我們就看看這兩條重復數據的回話ID吧

果然不出所料!!!


是不同的回話插入的,這也就解釋了為么在判斷時候加了行級排它鎖,卻仍然鎖不住記錄的原因
並發插入的時候,因為各個回話是取數據庫中檢測記錄,數據庫中不存在就插入,卻忽視了各個回話之間可能存在的重復值
假如是唯一索引,回話之間也是需要等待的,確保索引的唯一性。
這也就解釋了,用行級排它鎖“鎖定”一行記錄的時候,在鎖定的條件上建議唯一索引的原因。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM