mysql事務鎖
|--表級鎖(鎖定整個表)
|--頁級鎖(鎖定一頁)
|--行級鎖(鎖定一行)
|--共享鎖(S鎖,MyISAM 叫做讀鎖)
|--排他鎖(X鎖,MyISAM 叫做寫鎖)
|--悲觀鎖(抽象性,不真實存在這個鎖)
|--樂觀鎖(抽象性,不真實存在這個鎖)
樂觀鎖
樂觀鎖不是資料庫自帶的,需要我們自己去實現。樂觀鎖是指操作資料庫時(更新操作),想法很樂觀,認為這次的操作不會導致衝突,在操作資料時,並不進行任何其他的特殊處理(也就是不加鎖),而在進行更新後,再去判斷是否有衝突了。
通常實現是這樣的:在表中的資料進行操作時(更新),先給資料表加一個版本(version)欄位,每操作一次,將那條記錄的版本號加1。也就是先查詢出那條記錄,獲取出version欄位,如果要對那條記錄進行操作(更新),則先判斷此刻version的值是否與剛剛查詢出來時的version的值相等,如果相等,則說明這段期間,沒有其他程式對其進行操作,則可以執行更新,將version欄位的值加1;如果更新時發現此刻的version值與剛剛獲取出來的version的值不相等,則說明這段期間已經有其他程式對其進行操作了,則不進行更新操作。
舉例:
下單操作包括3步驟:
1.查詢出商品資訊
select (status,status,version) from t_goods where id=#{id}
2.根據商品資訊生成訂單
3.修改商品status為2
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
除了自己手動實現樂觀鎖之外,現在網上許多框架已經封裝好了樂觀鎖的實現,如hibernate,需要時,可能自行搜尋"hiberate 樂觀鎖"試試看。
案例:
某商品,使用者購買後庫存數應-1,而某兩個或多個使用者同時購買,此時三個執行程式均同時讀得庫存為n,之後進行了一些操作,最後將均執行update table set 庫存數=n-1,那麼,很顯然這是錯誤的。
解決:
1.使用悲觀鎖(其實說白了也就是排他鎖)
|--程式A在查詢庫存數時使用排他鎖(select * from table where id=10 for update)
|--然後進行後續的操作,包括更新庫存數,最後提交事務。
|--程式B在查詢庫存數時,如果A還未釋放排他鎖,它將等待。
|--程式C同B……
2.使用樂觀鎖(靠表設計和程式碼來實現)
|--一般是在該商品表新增version版本欄位或者timestamp時間戳欄位
|--程式A查詢後,執行更新變成了:
update table set num=num-1 where id=10 and version=23
這樣,保證了修改的資料是和它查詢出來的資料是一致的,而其他執行程式未進行修改。當然,如果更新失敗,表示在更新操作之前,有其他執行程式已經更新了該庫存數,那麼就可以嘗試重試來保證更新成功。為了儘可能避免更新失敗,可以合理調整重試次數(阿里巴巴開發手冊規定重試次數不低於三次)。
總結:對於以上,可以看得出來樂觀鎖和悲觀鎖的區別。
1.悲觀鎖使用了排他鎖,當程式獨佔鎖時,其他程式就連查詢都是不允許的,導致吞吐較低。如果在查詢較多的情況下,可使用樂觀鎖。
2.樂觀鎖更新有可能會失敗,甚至是更新幾次都失敗,這是有風險的。所以如果寫入較頻繁,對吞吐要求不高,可使用悲觀鎖。
也就是一句話:讀用樂觀鎖,寫用悲觀鎖。
共享鎖與排他鎖
共享鎖
又稱為讀鎖,獲得共享鎖之後,可以檢視但無法修改和刪除資料。
排他鎖,又稱為寫鎖、獨佔鎖。獲准排他鎖後,既能讀資料,又能修改資料。
排他鎖指的是一個事務在一行資料加上排他鎖後,其他事務不能再在其上加其他的鎖。
mysql InnoDB引擎預設的修改資料語句,update,delete,insert都會自動給涉及到的資料加上排他鎖,
select語句預設不會加任何鎖型別,如果加排他鎖可以使用select ...for update語句,加共享鎖可以使用select ... lock in share mode語句。所以加過排他鎖的資料行在其他事務種是不能修改資料的,也不能通過for update和lock in share mode鎖的方式查詢資料,但可以直接通過select ...from...查詢資料,因為普通查詢沒有任何鎖機制。
例1:-------------------------------------------------------------------------------------------------------------------------------------
T1:select * from table lock in share mode(假設查詢會花很長時間,下面的例子也都這麼假設)
T2:update table set column1='hello'
過程:
T1執行(並加共享鎖)
T2執行
If T1還沒執行完
T2等......
else鎖被釋放
T2執行
endif
T2 之所以要等,是因為 T2 在執行 update 前,試圖對 table 表加一個排他鎖,而資料庫規定同一資源上不能同時共存共享鎖和排他鎖。所以 T2 必須等 T1 執行完,釋放了共享鎖,才能加上排他鎖,然後才能開始執行 update 語句。
---------------------
例2:-------------------------------------------------------------------------------------------------------------------------------------
T1:select * from table lock in share mode
T2:select * from table lock in share mode
這裡T2不用等待T1執行完,而是可以馬上執行。
分析:
T1執行,則 table 被加鎖,比如叫lockAT2執行,再對 table 加一個共享鎖,比如叫lockB兩個鎖是可以同時存在於同一資源上的(比如同一個表上)。這被稱為共享鎖與共享鎖相容。這意味著共享鎖不阻止其它人同時讀資源,但阻止其它人修改資源。
例3:-------------------------------------------------------------------------------------------------------------------------------------
T1:select * from table lock in share mode
T2:select * from table lock in share mode
T3:update table set column1='hello'
T2 不用等 T1 執行完就能執行,T3 卻要等 T1 和 T2 都執行完才能執行。因為 T3 必須等 T1 和 T2 的共享鎖全部釋放才能進行加排他鎖然後執行 update 操作。
例4:(死鎖的發生)-----------------------------------------------------------------------------------------------------------------
T1:begin transelect * from table lock in share modeupdate table set column1='hello'
T2:begin transelect * from table lock in share modeupdate table set column1='world'
假設 T1 和 T2 同時達到 select,T1 對 table 加共享鎖,T2 也對 table 加共享鎖,當 T1 的 select 執行完,準備執行 update 時,根據鎖機制,T1 的共享鎖需要升級到排他鎖才能執行接下來的 update.在升級排他鎖前,必須等 table 上的其它共享鎖(T2)釋放,同理,T2 也在等 T1 的共享鎖釋放。於是死鎖產生了。
例5:-------------------------------------------------------------------------------------------------------------------------------------
T1:begin tranupdate table set column1='hello' where id=10
T2:begin tranupdate table set column1='world' where id=20
這種語句雖然最為常見,很多人覺得它有機會產生死鎖,但實際上要看情況
|--如果id是主鍵(預設有主鍵索引),那麼T1會一下子找到該條記錄(id=10的記錄),然後對該條記錄加排他鎖,T2,同樣,一下子通過索引定位到記錄,然後對id=20的記錄加排他鎖,這樣T1和T2各更新各的,互不影響。T2也不需要等。
|--如果id是普通的一列,沒有索引。那麼當T1對id=10這一行加排他鎖後,T2為了找到id=20,需要對全表掃描。但因為T1已經為一條記錄加了排他鎖,導致T2的全表掃描進行不下去(其實是因為T1加了排他鎖,資料庫預設會為該表加意向鎖,T2要掃描全表,就得等該意向鎖釋放,也就是T1執行完成),就導致T2等待。
死鎖怎麼解決呢?一種辦法是,如下:
例6:-------------------------------------------------------------------------------------------------------------------------------------
T1:begin transelect * from table for updateupdate table set column1='hello'
T2:begin transelect * from table for updateupdate table set column1='world'
這樣,當 T1 的 select 執行時,直接對錶加上了排他鎖,T2 在執行 select 時,就需要等 T1 事物完全執行完才能執行。排除了死鎖發生。但當第三個 user 過來想執行一個查詢語句時,也因為排他鎖的存在而不得不等待,第四個、第五個 user 也會因此而等待。在大併發情況下,讓大家等待顯得效能就太友好了。
所以,有些資料庫這裡引入了更新鎖(如Mssql,注意:Mysql不存在更新鎖)。
例7:-------------------------------------------------------------------------------------------------------------------------------------
T1:begin transelect * from table (加更新鎖)update table set column1='hello'
T2:begin transelect * from table (加更新鎖)update table set column1='world'
更新鎖其實就可以看成排他鎖的一種變形,只是它也允許其他人讀(並且還允許加共享鎖)。但不允許其他操作,除非我釋放了更新鎖。T1 執行 select,加更新鎖。T2 執行,準備加更新鎖,但發現已經有一個更新鎖在那兒了,只好等。當後來有 user3、user4...需要查詢 table 表中的資料時,並不會因為 T1 的 select 在執行就被阻塞,照樣能查詢,相比起例6,這提高了效率。
後面還有意向鎖和計劃鎖:意向鎖即是:某行修改時,自動加上了排他鎖,同時會預設給該表加意向鎖,表示裡面有記錄正被鎖定,這時,其他人就不可以對該表加表鎖了。如果沒有意向鎖這個類似指示燈的東西存在,其他人加表鎖之前就得掃描全表,檢視是否有記錄正被鎖定,效率低下。而計劃鎖這些,和程式設計師關係不大,就沒去了解了。
表鎖:
寫鎖:知道unlock 才會返回查詢值: