最近工作非常鬱悶,天天被領導盯著。主要是系統近來死鎖發生在頻率很高。最終,經過大家的共同努力,我們成功的定位並解決了問題,所以把過程中學習的知識與經驗分享一下
問題背景
系統中有乙個賬戶模組,負責管理和維護會員的各種資金及明細,對外的功能涉及資金的增加與扣減等。通過監控系統發現,當外圍系統併發訪問和呼叫同乙個會員的賬戶功能介面的時候,就會發生死鎖和響應超時的情況,所以問題的分析還得從事務的併發說起。首先,我們先回顧一下事務。
事務是神馬
所謂事務,它是乙個操作序列,這些操作要麼都執行,要麼都不執行,它是乙個不可分割的原子單位。例如,銀行轉帳工作:從乙個帳號扣款並使另乙個帳號增款,這兩個操作要麼都執行,要麼都不執行。
事務的特性
四個特性中,與我們問題相關的應該就是事務的隔離性了,要理解隔離性,就不得不說說事務的併發了。
事務的併發及帶來的影響
事務併發通俗點講就是多個事務同時執行,併發的訪問或更新資料庫中相同的資料。事務併發一般需要相應的隔離措施,否則就會出現各種問題。
事務的併發會造成哪些問題?
知道了事務併發會造成什麼影響,那如何避免問題的發生呢?從資料庫層面來講,是通過封鎖協議再解決的。
資料庫封鎖協議
在說封鎖協議前,先得講講鎖。
資料庫中有兩種鎖:共享鎖(讀鎖s)與排它鎖(寫鎖x)。
再來看看封鎖協議
封鎖協議是資料庫內部實現事務併發控制的一種機制,我們無法在程式中使用。在程式開發中,是通過指定事務的
隔離級別來解決併發的各種問題的。
事務的隔離級別
有四種隔離級別,無論哪一種都不會出現
丟失更新,因為四種隔離級別都要求在更新資料物件前先要對資料加x鎖,直到事務結束後才釋放,即一級封鎖協議。
這樣看來,事務隔離級別好像與封鎖協議是一一對應的。讀未提交與一級封鎖協議對應、讀已提交與二級封鎖協議對應、可重複讀與**封鎖協議對應。但經過本人的驗證,這個不完全正確,隔離級別是乙個規範,而封議協議是規範的一種實現方式。比如mysq同時使用資料行版本與鎖的機制來實現隔離級別的,db2單純使用鎖的機制來實現隔離級別。我會重新整理一篇文章講講關於mysql與db2在隔離級別方面的細節與區別。
我們已經知道了相關的知識背景,再來看看我們系統的問題。
系統使用的是db2,所有唯讀事務使用ur隔離級別(相關於讀未提交),其他事務使用rs隔離級別(相關於可重複讀)。會員的資產功能簡單講有增加和扣減操作。
增加操作
start transaction
//第一步,查詢賬戶可用餘額
select ... from account where account_id = ...
//第二步,餘量加上增加量,並賦值給新變數newbalance
set newbalance = dbbalance + changeamount
//第三步,更新會員賬戶記錄
update account set balance = newbalance where account_id = ...
//第四步,其它操作
commit
扣減操作
start transaction
//第一步,查詢賬戶可用餘額
select ... from account where account_id = ...
//第二步,餘量扣去增加量,並賦值給新變數new_balance,如果餘量不足則報錯。
set newbalance = dbbalance - changeamount
if ( newbalance < 0 ) throw new exception("...");
//第三步,更新會員賬戶記錄
update account set balance = newbalance where account_id = ...
//第四步,其它操作
commit
上面使用偽語言描述,可以看出,不論是增加還是扣減操作,都是先查詢出賬戶記錄,根據記錄值作計算,最後再更新記錄。當外圍系統同時訪問同一會員做扣減的時候,死鎖就有可能發生了,比如:
事務t1執行第一步,查詢了會員的賬戶記錄,因為隔離級別是rs,所以會對會員賬戶記錄加s鎖;這時候事務t2也執行了第一步,也對會員的賬戶記錄加了s鎖;之後事務t1執行第三步,在準備更新會員賬戶記錄前需要先對其加x鎖,但發現記錄已經被其它事務(事務t2)加了s鎖,所以事務t1掛起,等待事務t2釋放賬戶記錄的s鎖;接著事務t2也執行到第三步,在準備更新會員賬戶記錄前需要先對其加x鎖,但發現記錄已經被其它事務(事務t1)加了s鎖,所以事務t2也掛起等待事務t1。這樣死鎖就發生了。
知道了死鎖發生的原因,那現在看看如何解決這個問題。死鎖發生的原因主要是第一步查詢的時候對賬戶記錄加了s鎖,所以如何我們把隔離級別降為cs(相當於讀已提交),能否解決問題呢?答案是否,把隔離級底降為cs確實可以避免死鎖的發生,因為查詢操作結束後就會釋放s鎖,但是卻會發生
覆蓋更新的問題,所以這個方案不可行。既然問題出在第一步,那就是第一步出法看看吧,如果查詢賬戶的時候,我們顯示的對記錄加x鎖而不是s鎖,那問題就解決了?看看:
事務t1執行第一步,查詢了會員的賬戶記錄,顯式對會員賬戶記錄加x鎖;這時候事務t2執行到第一步,在查詢會員賬戶記錄嘗試加x鎖時會等待,因為記錄已經被別的事務(事務t1)加了x鎖;之後事務t1執行第三步,順利的更新了記錄,因為它已經占有記錄的x鎖;在事務t1提交之後,事務t2就可以繼續往下執行了,所以死鎖的問題解決了。
優化後的扣減操作
start transaction
//第一步,查詢賬戶可用餘額
select ... from account where account_id = ... for update with rs
//第二步,餘量扣去增加量,並賦值給新變數new_balance,如果餘量不足則報錯。
set newbalance = dbbalance - changeamount
if ( newbalance < 0 ) throw new exception("...");
//第三步,更新會員賬戶記錄
update account set balance = newbalance where account_id = ...
//第四步,其它操作
commit
對於增加操作來講,因為沒有上限限制,所以可以直接更新增加量就可以了。優化後的增加操作
start transaction
//第一步,更新會員賬戶記錄
update account set balance = balance + changeamount where account_id = ...
//第二步,其它操作
commit
經過上面兩個優化,死鎖不再發生了,另外介面的平均響應時間也有不小的提高。 事務併發 事務隔離級別
併發問題可歸納為以下幾類 a.丟失更新 撤銷乙個事務時,把其他事務已提交的更新資料覆蓋 a和b事務併發執行,a事務執行更新後,提交 b事務在a事務更新後,b事務結束前也做了對該行資料的更新操作,然後回滾,則兩次更新操作都丟失了 b.髒讀 乙個事務讀到另乙個事務未提交的更新資料 a和b事務併發執行,b...
事務併發 事務隔離級別
併發問題可歸納為以下幾類 a.丟失更新 撤銷乙個事務時,把其他事務已提交的更新資料覆蓋 a和 b事務併發執行,a事務執行更新後,提交 b事務在 a事務更新後,b事務結束前也做了對該行資料的更新操作,然後回滾,則兩次更新操作都丟失了 b.髒讀 乙個事務讀到另乙個事務未提交的更新資料 a和 b事務併發執...
併發與事務
本部落格只是在開發過程中,對遇到的多執行緒問題的思考,如何在保證資料正確的前提下,提高效能。我覺得併發要考慮兩個問題 在io層次,併發鏈結數過多例如c10k,c10m的問題,是通過reactor 模式解決?例如開源的網路庫都是使用單執行緒io復用 非阻塞的思想解決,最優!還是通過乙個連線對應乙個執行...