分佈式系統敏感操做的併發處理(併發鎖)

在實際工做中常常遇到對帳戶的操做(帳戶充值和帳戶消費),處理的邏輯以下:html

// 1 查詢帳戶當前的金額
// 2 根據操做,計算操做後的金額
// 3 更新帳戶的金額

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

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

  • 事務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。sql

按照業務的邏輯,消費和充值後,帳戶的金額應該爲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併發鎖

也就是,操做前要得到鎖,操做完成釋放鎖;沒有得到鎖,不容許進行操做,直接返回併發錯誤。code

在實際系統中,每每是分佈式部署的,那麼就須要加分佈式鎖。最容易想到(本人)的就是使用redis,在redis中使用setnx,僞代碼以下:htm

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實現分佈式併發鎖的。

相關文章
相關標籤/搜索