在實(shí)際的多用戶并發(fā)訪問(wèn)的生產(chǎn)環(huán)境里邊,我們經(jīng)常要盡可能的保持?jǐn)?shù)據(jù)的一致性。而其中
最典型的例子就是我們從表里邊讀取數(shù)據(jù),檢查驗(yàn)證后對(duì)數(shù)據(jù)進(jìn)行修改,然后寫回到數(shù)據(jù)庫(kù)
中。在讀取和寫入的過(guò)程中,如果在多用戶并發(fā)的環(huán)境里邊,其他用戶已經(jīng)把你要修改的數(shù)據(jù)
進(jìn)行了修改是非常有可能發(fā)生的情況,這樣就造成了數(shù)據(jù)的不一致性。解決這樣的辦法,sql server
提出了樂(lè)觀鎖定和悲觀鎖定的概念,下邊我以一個(gè)實(shí)例來(lái)說(shuō)明如何使用樂(lè)觀鎖定和悲觀鎖定來(lái)
解決這樣的問(wèn)題。
/* 建立測(cè)試表:card,代表一個(gè)真實(shí)的卡庫(kù),供用戶注冊(cè).用戶要從里邊選出一個(gè)未使用的卡,也就是f_flag=0的卡,給用戶注冊(cè):更新f_name,f_time,f_flag字段. 如果出現(xiàn)兩個(gè)用戶同時(shí)更新一張卡的情況,是不能容忍的,也就是我們所說(shuō)的數(shù)據(jù)不一致行。*/
create table card(f_cardno varchar(20),f_name varchar(20),f_flag bit,f_time datetime)
go
insert card(f_cardno,f_flag) select '1111-1111',0
insert card(f_cardno,f_flag) select '1111-1112',0
insert card(f_cardno,f_flag) select '1111-1113',0
insert card(f_cardno,f_flag) select '1111-1114',0
insert card(f_cardno,f_flag) select '1111-1115',0
insert card(f_cardno,f_flag) select '1111-1116',0
insert card(f_cardno,f_flag) select '1111-1117',0
insert card(f_cardno,f_flag) select '1111-1118',0
insert card(f_cardno,f_flag) select '1111-1119',0
insert card(f_cardno,f_flag) select '1111-1110',0
go
-- 下邊是我們經(jīng)常使用的更新方案如下:
declare @cardno varchar(20)
begin tran
       --  選擇一張未使用的卡
        select top 1 @cardno=f_cardno
        from card    where f_flag=0
        
        --  延遲50秒,模擬并發(fā)訪問(wèn).
        waitfor delay '000:00:50'
-- 把剛才選擇出來(lái)的卡進(jìn)行注冊(cè).
        update card
        set f_name=user,
            f_time=getdate(),
            f_flag=1
        where [email protected]
commit
問(wèn)題:如果我們?cè)谕淮翱趫?zhí)行同一段代碼,但是去掉了waitfor delay子句.兩邊執(zhí)行完畢后 我們發(fā)現(xiàn)盡管執(zhí)行了兩次注冊(cè),但是只注冊(cè)了一張卡,也就是兩個(gè)人注冊(cè)了同一張卡.
悲觀鎖定解決方案
-- 我們只要對(duì)上邊的代碼做微小的改變就可以實(shí)現(xiàn)悲觀的鎖定.
declare @cardno varchar(20)
begin tran
       --  選擇一張未使用的卡
        select top 1 @cardno=f_cardno
        from card   with (updlock)  where f_flag=0
        
        --  延遲50秒,模擬并發(fā)訪問(wèn).
        waitfor delay '000:00:50'
-- 把剛才選擇出來(lái)的卡進(jìn)行注冊(cè).
        update card
        set f_name=user,
            f_time=getdate(),
            f_flag=1
        where [email protected]
commit
注意其中的區(qū)別了嗎?with(updlock),是的,我們?cè)诓樵兊臅r(shí)候使用了with (updlock)選項(xiàng),在查詢記錄的時(shí)候我們就對(duì)記錄加上了更新鎖,表示我們即將對(duì)次記錄進(jìn)行更新.注意更新鎖和共享鎖是不沖突的,也就是其他用戶還可以查詢此表的內(nèi)容,但是和更新鎖和排它鎖是沖突的.所以其他的更新用戶就會(huì)阻塞.如果我們?cè)诹硗庖粋€(gè)窗口執(zhí)行此代碼,同樣不加waifor delay子句.兩邊執(zhí)行完畢后,我們發(fā)現(xiàn)成功的注冊(cè)了兩張卡.可能我們已經(jīng)發(fā)現(xiàn)了悲觀鎖定的缺點(diǎn):當(dāng)一個(gè)用戶進(jìn)行更新的事務(wù)的時(shí)候,其他更新用戶必須排隊(duì)等待,即使那個(gè)用戶更新的不是同一條記錄.
樂(lè)觀鎖定解決方案
-- 首先我們?cè)赾ard表里邊加上一列f_timestamp 列,該列是varbinary(8)類型.但是在更新的時(shí)候這個(gè)值會(huì)自動(dòng)增長(zhǎng).
alter table card add  f_timestamp timestamp not null
--  悲觀鎖定
declare @cardno varchar(20)
declare @timestamp varbinary(8)
declare @rowcount int
begin tran
       --  取得卡號(hào)和原始的時(shí)間戳值
        select top 1 @cardno=f_cardno,
                     @timestamp=f_timestamp
        from card
        where f_flag=0
        
        --  延遲50秒,模擬并發(fā)訪問(wèn).
        waitfor delay '000:00:50'
-- 注冊(cè)卡,但是要比較時(shí)間戳是否發(fā)生了變化.如果沒(méi)有發(fā)生變化.更新成功.如果發(fā)生變化,更新失敗.
        update card
        set f_name=user,
            f_time=getdate(),
            f_flag=1
        where [email protected] and [email protected]
        set @[email protected]@rowcount
        if @rowcount=1
        begin
                print '更新成功!'
                commit
        end
        else if @rowcount=0
        begin
                if exists(select 1 from card where [email protected])
                begin
                        print '此卡已經(jīng)被另外一個(gè)用戶注冊(cè)!'
                        rollback tran
                end
                else
                begin
                        print '并不存在此卡!'
                        rollback tran
                end
        end
在另外一個(gè)窗口里邊執(zhí)行沒(méi)有waitfor的代碼,注冊(cè)成功后,返回原來(lái)的窗口,我們就會(huì)發(fā)現(xiàn)到時(shí)間后它顯示的提示是此卡以被另外一個(gè)用戶注冊(cè)的提示.很明顯,這樣我們也可以避免兩個(gè)用戶同時(shí)注冊(cè)一張卡的現(xiàn)象的出現(xiàn).同時(shí),使用這種方法的另外一個(gè)好處是沒(méi)有使用更新鎖,這樣增加的系統(tǒng)的并發(fā)處理能力.
上邊我詳細(xì)介紹了樂(lè)觀鎖定和悲觀鎖定的使用方法,在實(shí)際生產(chǎn)環(huán)境里邊,如果并發(fā)量不大,我們完全可以使用悲觀鎖定的方法,因?yàn)檫@種方法使用起來(lái)非常方便和簡(jiǎn)單.但是如果系統(tǒng)的并發(fā)非常大的話,悲觀鎖定會(huì)帶來(lái)非常大的性能問(wèn)題,所以我們就要選擇樂(lè)觀鎖定的方法.
如果大家發(fā)現(xiàn)文章里邊有什么錯(cuò)誤的地方,請(qǐng)及時(shí)提醒我,也歡迎有興趣的一起研究討論.
新聞熱點(diǎn)
疑難解答
圖片精選