在實際工做中常常遇到對帳戶的操做(帳戶充值和帳戶消費),處理的邏輯以下:html
// 1 查詢帳戶當前的金額 // 2 根據操做,計算操做後的金額 // 3 更新帳戶的金額
然而,在實際中常常會有併發操做的問題,下面經過在數據中執行SQL的方式,模擬下不作併發處理的狀況:mysql
數據庫是MySQL,隔離級別採用默認的可重複讀,表爲t_money,只有兩列:id、money,只有一條記錄id=1, money=1000。分別起兩個客戶端,模擬併發操做的行爲:redis
序號 | 事務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,這是絕對不能接受的!數據庫
將更新金額的語句,使用:併發
update t_money set money=money-100 where id=1;
update會使用「當前讀」,能夠讀取到其它事物未提交的數據。當前讀遇到其它事務的寫操做時,會被阻塞,引發當前讀的語句:分佈式
select ... for update; select ... lock in share mode; update delete insert
也就是,操做前要得到鎖,操做完成釋放鎖;沒有得到鎖,不容許進行操做,直接返回併發錯誤。code
在實際系統中,每每是分佈式部署的,那麼就須要加分佈式鎖。最容易想到(本人)的就是使用redis,在redis中使用setnx,僞代碼以下:htm
if(redis.setnx(id)){ // 加鎖成功 // 帳戶操做 } else { // 返回併發錯誤,由調用者處理後續邏輯(重試等) }
在方案1中,在加鎖失敗後,直接返回併發異常,調用方須要重試。實際上,第一次請求時,雖然不能得到鎖,可是可能在1s以後就能夠得到鎖了,咱們何不如稍微等待下再重試呢?事務
更加優雅的加鎖,僞代碼:
if (redis.setnx(id)) { // 加鎖成功 // 帳戶操做 } else { // 第一次加鎖失敗 Thread.sleep(1000); // 等待1s,也能夠等待並指定屢次重試 if (redis.setnx(id)) { // 帳戶操做 } else { // 返回併發錯誤 } }
對於redis實現併發鎖,有不少能夠研究的細節,好比:setnx成功後,系統掛了,後續加鎖就永遠不能成功了,該如何處理?更多細節,能夠看看他人是如何用redis實現分佈式併發鎖的。