分布式系統敏感操作的並發處理(並發鎖)


在實際工作中經常遇到對賬戶的操作(賬戶充值和賬戶消費),處理的邏輯如下:

// 1 查詢賬戶當前的金額
// 2 根據操作,計算操作后的金額
// 3 更新賬戶的金額

然而,在實際中經常會有並發操作的問題,下面通過在數據中執行SQL的方式,模擬下不做並發處理的情況:

數據庫是MySQL,隔離級別采用默認的可重復讀,表為t_money,只有兩列:id、money,只有一條記錄id=1, money=1000。分別起兩個客戶端,模擬並發操作的行為:

  • 事務1,賬戶消費100元
  • 事務2,賬戶充值200元
序號 事務1 事務2
1 start transaction;
2 start transaction;
3 select * from t_money where id=1;
4 select * from t_money wehre id=1;
5 update t_money set money=900 where id=1;
6 update t_money set money=1200 where id=1; (不能執行,被阻塞)
7 select * from t_money where id=1;
8 commit; (事務1執行commit后,被阻塞的update執行)
9 select * from t_money where id=1; select * from t_money where id=1;
10 commit;
11 select * from t_money where id=1; select * from t_money where id=1;

按照上面的步驟執行完成后,11步查出來賬戶id=1的money=1200。

按照業務的邏輯,消費和充值后,賬戶的金額應該為1100,而系統中id=1的賬戶金額居然為1200,這是絕對不能接受的!

解決方案

1. 利用MySQL的當前讀

將更新金額的語句,使用:

update t_money set money=money-100 where id=1;

update會使用“當前讀”,可以讀取到其它事物未提交的數據。當前讀遇到其它事務的寫操作時,會被阻塞,引起當前讀的語句:

select ... for update;
select ... lock in share mode;
update
delete
insert

2. redis並發鎖

也就是,操作前要獲得鎖,操作完成釋放鎖;沒有獲得鎖,不允許進行操作,直接返回並發錯誤。

在實際系統中,往往是分布式部署的,那么就需要加分布式鎖。最容易想到(本人)的就是使用redis,在redis中使用setnx,偽代碼如下:

if(redis.setnx(id)){
    // 加鎖成功
    // 賬戶操作
} else {
    // 返回並發錯誤,由調用者處理后續邏輯(重試等)
}

2. 優雅的redis並發鎖

在方案1中,在加鎖失敗后,直接返回並發異常,調用方需要重試。實際上,第一次請求時,雖然不能獲得鎖,但是可能在1s之后就可以獲得鎖了,我們何不如稍微等待下再重試呢?

更加優雅的加鎖,偽代碼:

if (redis.setnx(id)) {
    // 加鎖成功
    // 賬戶操作
} else {
    // 第一次加鎖失敗
    Thread.sleep(1000); // 等待1s,也可以等待並指定多次重試
    if (redis.setnx(id)) {
        // 賬戶操作
    } else {
        // 返回並發錯誤
    }
}

對於redis實現並發鎖,有很多可以研究的細節,比如:setnx成功后,系統掛了,后續加鎖就永遠不能成功了,該如何處理?更多細節,可以看看他人是如何用redis實現分布式並發鎖的。


免責聲明!

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



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