1. 程式人生 > >排他鎖和共享鎖

排他鎖和共享鎖

出差的時候看見一個做BO的大牛寫了這樣一個SQL。select name from user for update,看的我是一臉懵逼,完全沒有見過,好吧只能怪自己見識少了。

鎖的基本概念
  當多事務爭取一個資源時,有可能導致資料不一致,這個時候需要一種機制限制,並且將資料訪問順序化,用來保證資料庫資料的一致性,鎖就是其中的一種機制。我們可以用商場的試衣間來做個比喻,商場裡得每個試衣間都可供多個消費者使用,因此可能出現多個消費者同時試衣服需要使用試衣間,這時候就產生衝突了,為了避免衝突,試衣間裝了鎖(其實就是進去之後把門拴住),某一個試衣服的人在試衣間裡把鎖鎖住了,其他顧客就不能再從外面打開了,只能等待裡面的顧客,試完衣服,從裡面把鎖開啟,外面的人才能進去(網上找到的比喻,非常形象)。不過我想要是併發了就尷尬了,哈哈。

鎖的基本型別
  資料庫上的操作可以歸納為兩種:讀和寫。
  多個事務同時讀取一個物件的時候,是不會有衝突的。同時讀和寫,或者同時寫才會產生衝突。因此為了提高資料庫的併發效能,通常會定義兩種鎖:共享鎖和排它鎖。

共享鎖(Shared Lock,也叫S鎖)
  共享鎖(S)表示對資料進行讀操作。因此多個事務可以同時為一個物件加共享鎖。(如果試衣間的門還沒被鎖上,顧客都能夠同時進去參觀)
  產生共享鎖的sql:select * from ad_plan lock in share mode;

共享鎖的使用場景
  SELECT … LOCK IN SHARE MODE走的是IS鎖(意向共享鎖),即在符合條件的rows上都加了共享鎖,這樣的話,其他人可以讀取這些記錄,也可以繼續新增IS鎖,但是無法修改這些記錄直到你這個加鎖的過程執行完成(完成的情況有:事務的提交,事務的回滾,否則直接鎖等待超時)。
  SELECT … LOCK IN SHARE MODE的應用場景適合於兩張表存在關係時的寫操作,拿mysql官方文件的例子來說,一個表是child表,一個是parent表,假設child表的某一列child_id對映到parent表的c_child_id列,那麼從業務角度講,此時我直接insert一條child_id=100記錄到child表是存在風險的,因為剛insert的時候可能在parent表裡刪除了這條c_child_id=100的記錄,那麼業務資料就存在不一致的風險。正確的方法是再插入時執行select * from parent where c_child_id=100 lock in share mode,鎖定了parent表的這條記錄,然後執行insert into child(child_id) values (100)就不會存在這種問題了。

排他鎖(Exclusive Lock,也叫X鎖)

排他鎖也叫寫鎖(X)。

排他鎖表示對資料進行寫操作。如果一個事務對物件加了排他鎖,其他事務就不能再給它加任何鎖了。(某個顧客把試衣間從裡面反鎖了,其他顧客想要使用這個試衣間,就只有等待鎖從裡面給打開了)
產生排他鎖的sql: select * from ad_plan for update;看到了吧,for update出現了,所以for update 是排他鎖,漲知識了。
排他鎖的使用場景:

使用場景一:訂單的商品數量
    但是如果是同一張表的應用場景,舉個例子,電商系統中計算一種商品的剩餘數量,在產生訂單之前需要確認商品數量>=1,產生訂單之後應該將商品數量減1。
    1 select amount from product where product_name=‘XX’;
    2 update product set amount=amount-1 where product_name=‘XX’;

顯然1的做法是是有問題,因為如果1查詢出amount為1,但是這時正好其他session也買了該商品併產生了訂單,那麼amount就變成了0,那麼這時第二步再執行就有問題。那麼採用lock in share mode可行嗎,也是不合理的,因為兩個session同時鎖定該行記錄時,這時兩個session再update時必然會產生死鎖導致事務回滾。以下是操作範例(按時間順序)

使用場景一:資料表的狀態

如果存在一張表記錄一個商品的狀態,在訂單的變化過程中,訂單的狀態是不斷變化的,而且變化的過程中肯定也會有併發的問題,而且很多時候與其他系統有互動,會存在補償的情況,所以併發的可能性很大,補償或者為了增加狀態修改的成功可能性,2次改變狀態的情況也有,樓主就遇到了這種情況,真操蛋。於是看到有這樣的for update寫法。

1 update order set status = 1 where product_id = ‘1’;

2 insert order_flow (…) value (…)

這樣的情況下就有可能訂單的狀態已經更新完成了,但是補償這些額外的訊息把狀態又更新為待處理或者插入了多條流水的情況(多條流水的可能性大,狀態的那種可能補償滯後)。這個時候就可以使用select … from order where order_id = ‘1’ for update,先鎖住要修改狀態的表,這樣就不會別人操作了,自己先後面把流水插入,然後更新狀態,完美。但是加了鎖之後效能就很慢了,擔心效能影響,而且有可能存在死鎖的情況,後面我就修改為流水錶中增加一個唯一索引,這樣插入流水報錯就是已經處理過的記錄了。這樣就不會存在效能問題。

通過對比,lock in share mode適用於兩張表存在業務關係時的一致性要求,for update適用於操作同一張表時的一致性要求。