面試官:你知道大事務會帶來什么問題以及如何解決么?


什么是大事務?

簡單來說就是那些運行時間比較長,操作的數據比較多的事務

如何查詢大事務?

以查詢執行時間超過10秒的事務為例:

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>10

大事務一般會對數據庫造成什么問題?

鎖定數據過多,容易造成大量的死鎖和鎖超時

當系統中不同事務之間出現循環資源依賴,涉及的事務都在等待別的事務釋放資源時,就會導致這幾個事務都進入無限等待的狀態,比如下面這個場景:
file
這時候,事務A在等待事務B釋放id=2的行鎖,而事務B在等待事務A釋放id=1的行鎖。 事務A和事務B在互相等待對方的資源釋放,就是進入了死鎖狀態

首先我們知道,有兩種策略可以處理死鎖:

  • 等待死鎖超時。超時時間(innodb_lock_wait_timeout)默認是50s,這時間可以說真的是太長了,但是如果改小了吧,又可能會影響到本可以正常消除的死鎖
  • 死鎖檢測。死鎖檢測的配置默認是開啟的。死鎖檢測就是每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最后判斷是否出現了死鎖

但是死鎖檢測可能會存在一個問題:
假如所有事務都要更新同一行的時候。每個新來的被堵住的線程,都要判斷會不會由於自己的加入導致了死鎖,這是一個時間復雜度是 O(n)的操作。假設有1000個並發線程要同時更新同一行,那么死鎖檢測操作就是100萬這個量級 的。雖然最終檢測的結果是沒有死鎖,但是這期間要消耗大量的CPU資源。因此,你就會看到 CPU利用率很高,但是每秒卻執行不了幾個事務。

回滾記錄占用大量存儲空間,事務回滾時間長

在MySQL中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。
假設一個值從1被按順序改成了2、3、4,在回滾日志里面就會有類似下面的記錄。
file
當前值是4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的read-view。如圖中看到的,在視圖A、B、C里面,這一個記錄的值分別是1、2、4,同一條記錄在系統中可以存在多個版本,就是數據庫的多版本並發控制(MVCC)。對於read-view A,要得到1,就必須將當前值依次執行圖中所有的回滾操作得到
同時你會發現,即使現在有另外一個事務正在將4改成5,這個事務跟read-view A、B、C對應的 事務是不會沖突的。
你一定會問,回滾日志總不能一直保留吧,什么時候刪除呢?答案是,在不需要的時候才刪除。 也就是說,系統會判斷,當沒有事務再需要用到這些回滾日志時,回滾日志會被刪除
什么時候才不需要了呢?就是當系統里沒有比這個回滾日志更早的read-view的時候,換一種說法就是在這些事物提交之后。

執行時間長,容易造成主從延遲

因為主庫上必須等事務執行完成才會寫入binlog,再傳給備庫。所以,如果一個主庫上的語句執行10分鍾,那這個事務很可能就會導致從庫延遲10分鍾

解決方案

基於兩階段鎖協議

兩階段鎖協議是什么?
在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放

基於兩階段鎖協議我們可以做這樣的優化:
如果你的事務中需要鎖多個行,要把最可能造成鎖沖突、最可能影響並發度的鎖盡量往后放

假設你負責實現一個電影票在線交易業務,顧客A要在影院B購買電影票。我們簡化一點,這個 業務需要涉及到以下操作:

  1. 從顧客A賬戶余額中扣除電影票價;
  2. 給影院B的賬戶余額增加這張電影票價; 3. 記錄一條交易日志。
    也就是說,要完成這個交易,我們需要update兩條記錄,並insert一條記錄。當然,為了保證交易的原子性,我們要把這三個操作放在一個事務中。那么,你會怎樣安排這三個語句在事務中的 順序呢?
    試想如果同時有另外一個顧客C要在影院B買票,那么這兩個事務沖突的部分就是語句2了。因為 它們要更新同一個影院賬戶的余額,需要修改同一行數據。
    根據兩階段鎖協議,不論你怎樣安排語句順序,所有的操作需要的行鎖都是在事務提交的時候才 釋放的。所以,如果你把語句2安排在最后,比如按照3、1、2這樣的順序,那么影院賬戶余額 這一行的鎖時間就最少。這就最大程度地減少了事務之間的鎖等待,提升了並發度。

基於死鎖檢測

想要解決死鎖檢測的問題那么就只能控制O(n)的數量,當同一行並發數小的時候死鎖檢測的成本就會很低了

不過這個並發數還挺不好控制:

  1. 分布式系統中客戶端數量是不確定的,所以不能在客戶端做限制
  2. 在MySQL端做的話需要修改源碼,這個就非大牛而不可為了
  3. 參考JDK1.7的ConcurrentHashMap的分段鎖設計,將一行數據改成邏輯上的多行數據來減少鎖沖突
    其中第三個方案還是有點意思的,接着以影院的賬戶為例,可以將一個賬號信息放在多條記錄上,比如10個記錄,影院的賬戶總額等於這10個記錄的值的總和。這樣每次要給影院賬戶加金額的時候,隨機選其中一條記錄來加。這樣每次沖突概率變成原來的1/10,可以減少鎖等待個數,也就減少了死鎖檢測的CPU消耗。

這個方案看上去是無損的,但其實這類方案需要根據業務邏輯做詳細設計。如果賬戶余額可能會減少,比如退票邏輯,那么這時候就需要考慮當一部分行記錄變成0的時候,代碼要有特殊處理。

基於事務的隔離級別

我們知道MySQL的事務隔離級別默認是可重復讀,在這個隔離級別下寫數據的時候會有這些問題:

  1. 如果有索引(包括主鍵索引)的時候,以索引列為條件更新數據,會存在間隙鎖、行鎖、下一鍵鎖的問題,從而鎖住一些行
  2. 如果沒有索引,更新數據時會鎖住整張表

但是如果把隔離級別改為讀提交就不存在這兩個問題了,每次寫數據只會鎖一行

但同時,你要解決可能出現的數據和日志不一致問題,需要把binlog格式設置 為row

關於為什么要設置binlog可以參考這篇文章為什么要把MySQL的binlog格式修改為row

其它

  1. 一些只讀的操作就沒有必要開啟事物了
  2. 通過SETMAX_EXECUTION_TIME命令, 來控制每個語句執行的最長時間,避免單個語句意外執行太長時間
  3. 監控 information_schema.Innodb_trx表,設置長事務閾值,超過就報警/或者kill
  4. 在業務功能測試階段要求輸出所有的general_log,分析日志行為提前發現問題
  5. 設置innodb_undo_tablespaces值,將undo log分離到獨立的表空間。如果真的出現大事務導致回滾段過大,這樣設置后清理起來更方便。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM