mysql事務和鎖 如何應對秒殺搶購高併發思路

參考: 

1,如何應對秒殺搶購高併發思路

2, MySQL InnoDB中,樂觀鎖、悲觀鎖、共享鎖、排它鎖、行鎖、表鎖、死鎖概念的理解

一,鎖

鎖是併發控制中最核心的概念之一,在MySQL中的鎖分兩大類,一種是讀鎖,一種是寫鎖,讀鎖也能夠稱爲共享鎖(shared lock),寫鎖也一般稱爲排它鎖(exclusive lock)。php

  這裏先不討論鎖的具體實現,描述一下鎖的概念:讀鎖是共享的,或者說是相互不阻塞的。多個客戶在同一時刻能夠同時讀取一個資源,且互不干擾。寫鎖則是排他的,就是說一個寫鎖會阻塞其餘的寫鎖和讀鎖,這是出於安全策略的考慮,只有這樣,才能確保在給定時間裏,只有一個用戶能執行寫入,並防止其餘用戶讀取正在寫入的同一資源。另外在通常狀況下,寫鎖比讀鎖優先級高。html

  MySQL中的鎖有兩種粒度,一種是表鎖,在表級別加鎖,是MySQL中最基本的鎖策略,而且開銷最小,這種鎖的併發性能較低;另外一種爲行鎖,在行級加鎖,併發性較高。表鎖與行鎖沒有絕對的性能強弱之分,在應用中能夠根據實際場景選擇,在鎖粒度與數據安全之間尋求一種平衡機制。mysql

  InnoDB的行鎖是基於索引實現的,若是不經過索引訪問數據,InnoDB會使用表鎖。sql

  鎖的具體實現協議大致分爲兩種:顯式鎖和隱式鎖。顯式鎖是指根據用戶須要手動去請求的鎖。隱式鎖則是指存儲引擎自行根據須要施加的鎖。顯式鎖的用法示例:數據庫

  例1:開啓兩個ssh鏈接同一主機,進入MySQL,在鏈接A上對錶tbl2作讀鎖操做:緩存

1 mysql> USE mysql;
2 mysql> LOCK TABLE tbl2 READ;

  在鏈接B上讀取數據是能夠的,可是寫入數據不行:安全

1 mysql> USE mysql;
2 mysql> SELECT * FROM tbl2;
3 mysql> INSERT INTO tbl2 VALUES (1,'tom'); #會一直卡在這一步,不向後執行。

  當在鏈接1上將tbl2解鎖後,就能寫入數據了:session

 

 

二,事務

在php與數據庫的交互中,若是併發量大,而且都去進行數據庫的修改的話,就有一個問題須要注意.數據的鎖問題.就會牽扯數據庫的事務跟隔離機制

數據庫事務依照不一樣的事務隔離級別來保證事務的ACID特性,也就是說事務不是一開啓就能解決全部併發問題。一般狀況下,這裏的併發操做可能帶來四種問題:併發

  • 更新丟失:一個事務的更新覆蓋了另外一個事務的更新,這裏出現的就是丟失更新的問題。
  • 髒讀:一個事務讀取了另外一個事務未提交的數據。
  • 不可重複讀:一個事務兩次讀取同一個數據,兩次讀取的數據不一致。
  • 幻象讀:一個事務兩次讀取一個範圍的記錄,兩次讀取的記錄數不一致。

三,事務隔離級別

  事務的隔離級別有四種:oracle

1.READ UNCOMMITTED(未提交讀)

  在READ UNCOMMITTED級別,事務中的修改即便沒有提交,對其它事務也都是可見的。即事務可讀取未提交的數據,這稱爲髒讀(Dirty Read)。這會致使不少問題,在實際應用中通常不多用到。

2.READ COMMITED(提交讀)

  READ COMMITTED表示只能讀取事務修改提交後的數據。此級別有時候也叫作不可重複讀(nonrepeatable read),由於兩次執行一樣的查詢,可能會獲得不同的結果。

3.REPEATABLE READ(可重複讀)

  此級別解決了髒讀的問題,保證了在同一個事務中屢次一樣記錄的結果是一致的。但會帶來新的問題——幻讀(Phantom Read)。MySQL默認使用此級別。

4.SERIALIZABLE(可串行化)

  SERIALIZABLE是最高的隔離級別。它會強制事務串行執行,避免了幻讀、髒讀的問題,可是犧牲了併發性。

  示例:

mysql> SELECT @@session.tx_isolation; #查看當前事務隔離級別
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)

mysql> SET @@session.tx_isolation='級別'; 可設置隔離級別

 

四,樂觀鎖和悲觀鎖

樂觀鎖和悲觀鎖是一種機制不是指具體的鎖。

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。假定會發生併發衝突,屏蔽一切可能違反數據完整性的操做。

樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,樂觀鎖適用於讀多寫少的應用場景,這樣能夠提升併發粒度。

悲觀鎖實現:事務隔離級別的可串行化就是典型的悲觀鎖機制,讀加讀鎖,寫加寫鎖。這是基於鎖的併發控制。

樂觀鎖實現:innoDB引擎的樂觀鎖機制是經過MVCC實現的。叫多版本併發控制(讀不加鎖讀寫不衝突),是經過保存數據在某一個時間點的快照來實現的。所以每個事務不管執行多長時間看到的數據,都是同樣的。

增刪改操做都屬於當前讀,不會讀取歷史版本的快照數據。

普通的select屬於快照讀,讀取的是快照數據也多是歷史數據。

手動加鎖的select是當前讀 例如:select * from tb for update或者select * from tb lock in share mode

涉及搶購、秒殺、抽獎、搶票等活動時,爲了不超賣,那麼庫存數量是有限的,可是若是同時下單人數超過了庫存數量,就會致使商品超賣問題。那麼咱們怎麼來解決這個問題呢,個人思路以下(僞代碼): 

sql1:查詢商品庫存
if(庫存數量 > 0)
{
  //生成訂單...
  sql2:同時庫存-1
}

當沒有併發時,上面的流程看起來是再正常不過了,假設同時兩我的下單,而庫存只有1個了,在sql1階段兩我的查詢到的庫存都是>0的,因而最終都執行了sql2,庫存最後變爲-1,超售了,這不是咱們想要的結果吧。

解決這個問題比較流行的思路我總結了下:
1.用額外的單進程處理一個隊列,下單請求放到隊列裏,一個個處理,就不會有併發的問題了,可是要額外的開啓後臺進程以及延遲問題,這裏暫不予考慮。這裏我可以使用消息隊列,咱們經常使用到Memcacheq、Radis。 好比:有100張票可供用戶搶,那麼就能夠把這100張票放到緩存中,讀寫時不要加鎖。 當併發量大的時候,可能有500人左右搶票成功,這樣對於500後面的請求能夠直接轉到活動結束的靜態頁面。進去的500我的中有400我的是不可能得到商品的。因此能夠根據進入隊列的前後順序只能前100我的購買成功。後面400我的就直接轉到活動結束頁面。固然進去500我的只是舉個例子,至於多少能夠本身調整。而活動結束頁面必定要用靜態頁面,不要用數據庫。這樣就減輕了數據庫的壓力。
2.mysql樂觀鎖,意思是好比總庫存是2,搶購事件提交時,立馬將庫存+1,那麼此時庫存是3,而後訂單生成後,在更新庫存前再查詢一次庫存(由於訂單生成理所固然庫存-1,可是先不急,再查一次庫存返回結果是3),看看跟預期的庫存數量(這裏預期的庫存是3)是否保持一致,不一致就回滾,提示用戶庫存不足。這裏說道悲觀鎖,可能有朋友會問,那必定有樂觀鎖了吧??這裏我就淺談下我所瞭解的悲觀與樂觀鎖了

 

1,悲觀鎖(Pessimistic Lock)

悲觀鎖的特色是先獲取鎖,再進行業務操做,即「悲觀」的認爲獲取鎖是很是有可能失敗的,所以要先確保獲取鎖成功再進行業務操做。一般所說的「一鎖二查三更新」即指的是使用悲觀鎖。

一般來說在數據庫上的悲觀鎖須要數據庫自己提供支持,即經過經常使用的select … for update操做來實現悲觀鎖。當數據庫執行select for update時會獲取被select中的數據行的行鎖,所以其餘併發執行的select for update若是試圖選中同一行則會發生排斥(須要等待行鎖被釋放),所以達到鎖的效果。

select for update獲取的行鎖會在當前事務結束時自動釋放,所以必須在事務中使用。

這裏須要注意的一點是不一樣的數據庫對select for update的實現和支持都是有所區別的,例如oracle支持select for update no wait,表示若是拿不到鎖馬上報錯,而不是等待,mysql就沒有no wait這個選項。另外mysql還有個問題是select for update語句執行中全部掃描過的行都會被鎖上,這一點很容易形成問題。

所以若是在mysql中用悲觀鎖務必要肯定走了索引,而不是全表掃描。

2,樂觀鎖(Optimistic Lock)

在每次去拿數據的時候認爲別人不會修改,不對數據上鎖,可是在提交更新的時候會判斷在此期間數據是否被更改,若是被更改則提交失敗。

樂觀鎖實現:使用版本控制字段,再利用行鎖的特性實現樂觀鎖

有一張訂單表order,有字段id、order_no、 price,  爲實現樂觀鎖控制,添加version字段,默認值爲0

id 1
order_no 123456
price 5
version 0

假設兩我的同時進來修改該條數據,操做爲:

1. 先查詢該數據   select * from order where id = 1

2. 修改該條數據  update order set price = 1 where id = 1 and price = 5

若是兩我的同時查詢到該條數據price = 5, 能夠執行update操做, 但任意一方還沒執行update操做,那麼最後雙方都執行update,致使數據被修改兩次,這樣很容易產生錯誤數據(髒數據)

使用version字段控制版本後:

1. 兩人先查詢該數據 select * from order where id = 1

此時兩人查詢到的數據同樣,id = 1, price = 5, order_no = 123456, version = 0

2. 兩人都發現該條數據price = 5, 符合update條件,第一人執行update(由於mysql行鎖的特性,兩人不可能同時修改一條數據,因此update同一條數據的時候,是有前後順序的,只有在第一個執行完update,才能釋放行鎖,第二個繼續進行update): 

update order set price = 1, version = version + 1 where id = 1 and price = 5 and version = 0

執行完成後,version字段值將變成1, 第二人執行update:

update order set price = 1, version = version + 1 where id = 1 and price = 5 and version = 0

此時的version的值已經被修改成1,因此第二人修改失敗,實現樂觀鎖控制。

相關文章
相關標籤/搜索