程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 數據庫知識 >> MYSQL數據庫 >> MySQL綜合教程 >> 數據庫鎖機制,鎖機制

數據庫鎖機制,鎖機制

編輯:MySQL綜合教程

數據庫鎖機制,鎖機制


一、為什麼需要了解鎖

1.1 死鎖問題

1.2 並發問題導致的不正確數據的讀取和存儲,破壞數據一致性的

  • 丟失更新:當兩個或多個事務選擇同一行,然後基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題--最後的更新覆蓋了由其他事務所做的更新。例如,兩個編輯人員制作了同一文檔的電子副本。每個編輯人員獨立地更改其副本,然後保存更改後的副本,這樣就覆蓋了原始文檔。最後保存其更改副本的編輯人員覆蓋另一個編輯人員所做的更改。如果在一個編輯人員完成並提交事務之前,另一個編輯人員不能訪問同一文件,則可避免此問題
  • 髒讀:一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“髒”數據,並據此做進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象地叫做”髒讀”。
  • 不可重復讀:一個事務在讀取某些數據後的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生了改變、或某些記錄已經被刪除了!這種現象就叫做“不可重復讀”。
  • 幻讀:一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。

二、鎖的分類

2.1 數據庫維度

  • 共享鎖:用於不更改或不更新數據的操作(只讀操作)。共享鎖允許並發事務讀取同一個資源,數據資源上存在共享鎖時,任何其他事務不允許修改數據
  • 排它鎖: 用於數據修改,確保不會同時多重更新同一數據。資源上存在排他鎖時,其他任何事務不允許給資源上鎖,當資源上有其他鎖時,也無法對其加上排它鎖

PS:只有共享鎖與共享鎖相互兼容,共享鎖與排它鎖、排它鎖之間都互不兼容

  • 更新鎖 

2.2 數據庫鎖機制

DBMS SELECT UPDATE INSERT DELETE MySQL(InnoDB) 不加鎖 排它鎖 排它鎖 排它鎖 SQL SERVER 共享鎖 更新鎖  排它鎖 排它鎖

 

 

 

 

 這兩種鎖機制的區別在於MySQL的查詢與更新操作互相不阻塞;而SQL SERVER的更新鎖轉化成排它鎖之前,其查詢與更新操作互相不阻塞,當更新鎖要轉化為排它鎖時,需要等待共享鎖的釋放,當更新鎖轉化為排它鎖後,查詢數據需要等待排它鎖的釋放。

參考:
[數據庫鎖機制](http://blog.csdn.net/samjustin1/article/details/52210125)
[InnoDB鎖機制](http://blog.chinaunix.net/uid-24111901-id-2627857.html)
[SQL SERVER鎖機制](http://blog.itpub.net/13651903/viewspace-1091664/)

2.3 程序員思想維度

  • 悲觀鎖
  •  樂觀鎖
update table 
set date=1,version=version+1
where id=#{id} and version=#{version};

參考:
[樂觀鎖與悲觀鎖](http://www.open-open.com/lib/view/open1452046967245.html)

2.4 樂觀鎖另一種實現方式CAS

CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的沖突檢查+數據更新的原理是一樣的。

java.util.concurrent(J.U.C)就是建立在CAS之上的。相對於對於synchronized這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以J.U.C在性能上有了很大的提升。

public class AtomicInteger extends Number implements java.io.Serializable {
  private volatile int value; 

  public final int get() { 
    return value; 
  } 

  public final int getAndIncrement() { 
    for (;;) { 
      int current = get(); 
      int next = current + 1; 
      if (compareAndSet(current, next)) 
        return current; 
    } 
  } 

  public final boolean compareAndSet(int expect, int update) { 
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
  } 
}

 

參考:
[樂觀鎖的一種實現方式—CAS](http://www.importnew.com/20472.html)

三、案例分析

3.1 初審統計數據遷移

  • 遷移背景:原有的統計方式采用的是實時count的方法獲取統計數據,造成的問題是查詢慢且無法獲取長時間段的統計數據(sql超時)、無法獲取某日統計數據的快照(前一天的待審核數據會變成今天的審核通過數據)
  • 采用遷移方式:使用raptor遷移平台,掃描審核記錄表,取出累計統計數據後進行加1操作,然後更新到統計表中。由於平台特性,數據遷移過程具有高並發性,由於強行采用先讀取後更新的方式,會造成丟失更新的情況,於是這裡考慮采用CAS

step1:

 

select id,passCount,rejectCount,hideCount,warnCount,waitCount 
from book.TradeItemAuditCount 
where type = #{type} and date = #{date} and editor = #{editor} 
and isDeleted = 0 limit 1

step 2:【失敗重試】

update book.TradeItemAuditCount 
set passCount = #{passCount} , rejectCount = #{rejectCount} , hideCount = #{hideCount} , warnCount = #{warnCount} , waitCount = #{waitCount} , updated = #{updated}
where id = #{id} and passCount = #{oldPassCount} and rejectCount = #{oldRejectCount} and hideCount = #{oldHideCount} and warnCount = #{oldWarnCount} and waitCount = #{oldWaitCount}
and isDeleted = 0 limit 1
  •  處理結果 :一共掃描審核結果4335668條數據,對重試次數超過100的更新操作進行記錄,發現更新操作出現大部分的重試,任務本身DB寫操作的qps較低【都不需要通過控制台限制速率..】

3.2 商品庫存

  • 商品庫存與上述案例1一致,都是對數據記錄進行加減操作,發現庫存的更新方式如下:
update 庫存表
set stock=stock-1
where id=#{id}
  • 直接使用數據庫的排它鎖就簡單的避免了並發導致的丟失更新問題,之前提到的一次只有一個事務擁有資源的排它鎖,並發的更新操作都試圖占有資源的排它鎖,當資源上存在排它鎖時,其他更新操作需要等待鎖的釋放
  • 相比案例1的解決方案,案例2的解決方式直接使用了MySQL InnoDB更新操作本身就擁有的排它鎖,不需要額外的開銷,而案例1不必要的查詢操作以及多次的重試操作嚴重影響到了數據遷移的性能,所以案例1是反面例子..

3.3 商品打標

  • 隨著上打標的qps上漲,出現達標更新數據丟失的情況
{"tags":"16,32,233,22","itemState":1,"hd":"ai:4|nd:18","au":"baoming"}
  • 處理方案

  1.樂觀鎖:采用CAS

update TradeItem
set extra=#{extra}
where tradeItemId=#{tradeItemId} and extra=#{oldExtra}

這裡使用長字符串做更新條件,會影響到SQL性能

  2.樂觀鎖:采用數據版本 表中新增version字段標識數據版本,作為數據更新的檢查方式

update TradeItem
set extra=#{extra} , version=version+1
where tradeItemId=#{tradeItemId} and version=#{version}

此方案改造較大,還需要為表新增字段,而且采用樂觀鎖擁有這一律的弊端:重試帶來的時間代價,一旦並發量上漲,某次更新操作的重試次數也會隨之上漲,直接影響到暴露服務的響應時間。【限制重試次數能夠一定程度上控制更新操作的響應時間,但是仍然會出現更新丟失的現象(讓調用方進行重試操作,分攤單次請求的響應時間?)】  

  3. 悲觀鎖:更新丟失的根本原因是執行查詢、修改兩個操作之間數據被另一事務修改了,單純的UPDATE操作其實也是進行著先查詢後修改的操作,沒有產生更新丟失是因為數據上存在排它鎖(sql server則是更新鎖),在執行期間並不允許其他修改。同理我們將要打標的商品記錄加上排它鎖或者更新鎖就能解決問題。 

MySQL:

start transaction;
SELECT extra
FROM TradeItem 
WHERE tradeItemId=#{tradeItemId}
FOR UPDATE;
UPDATE TradeItem 
SET extra = bdo.AddTag(tag,extra)
WHERE tradeItemId=#{tradeItemId};
commit;

SQL SERVER:

BEGIN TRANSACTION --開始一個事務
SELECT extra
FROM TradeItem WITH (UPDLOCK)
WHERE tradeItemId=#{tradeItemId}
UPDATE TradeItem 
SET extra = bdo.AddTag(tag,extra)
WHERE tradeItemId=#{tradeItemId}
COMMIT TRANSACTION --提交事務

該方案避免了重試帶來的開銷,同時使用排它鎖(更新鎖)也沒有額外增加鎖的開銷

四、悲觀鎖樂觀鎖的取捨

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved