併發操作餘額,給996留點福報

小姐姐味道發表於2019-04-15

想象一下你在一個月黑風高的夜晚,大概是10點多鐘的樣子,加班歸來,打算到小賣部弄盒煙抽。 夜涼風急,你用力裹了下被風鼓起的外套。 那裡有你暗戀的收銀姑娘。 沒日沒夜的工作,只有這十幾分鍾,能讓你感到些許生活的意義。 從羞澀的錢包裡翻出僅存的一張百元大鈔,結賬。然後用顫抖的雙手接過收銀員的找零。 不是因為輕觸到了她的指尖。 也並非因她如花的笑靨。

只因為,腦海裡竟然不爭氣的浮現出這樣的過程。

balance = dao.getBalance(userid)
balance = balance - cost
dao.setBalance(userid,balance)
複製程式碼

還真是狗改不了吃屎啊,果然還是一個碼畜。提醒著自己,自卑的埋下了臉,快步走開。

這是什麼?這是我們送給996公司的一點福報。

一波麻6的操作

餘額修改,是交易系統裡最常見的操作。上面的虛擬碼,大意是先取出餘額,然後扣掉消費,然後再回寫餘額。通常情況下這不會發生問題。

除非是高併發,與你是否單機無關。

對單一餘額的高併發操作自然不是正常人發起的,系統正在承受攻擊,或者自以為是的使用了MQ。在攻擊面前,上面的操作顯得不堪一擊。

拿一個最嚴重的例子說明:同時發起了一筆消費20元和消費5元的請求。在經過一波猛如虎的操作之後,兩個請求都支付成功了,但只扣除了5元。相當於花了5塊錢,買了25的東西。

併發操作餘額,給996留點福報
劃重點:把以上操作擴充套件到提現操作上,就更加的恐怖。比如你發起了一筆100元的提現和0.01元的提現,結果餘額被扣減0.01元的提現給覆蓋了。這相當於你有了一個提款機,非要薅到平臺倒閉為止。

防護辦法

通過SQL解決

update user set balance = balance - 20
    where userid=id
複製程式碼

這條語句就保險了很多,如果考慮到餘額不能為負的情況,可以把sql更加精進一點。

update user set balance = balance - 20
    where userid=id and balance >= 20
複製程式碼

以上sql,就可以保證餘額的安全,高併發下的攻擊就變得意義不大了。​但會有別的問題,比如重複扣款。

通過事務解決

現實中,這種直接通過sql扣減的應用,規模都比較小。當你的業務逐漸複雜,又沒有進行很好的拆分的情況下,先讀再設值的情況還是比較普遍的。比如某些營銷操作、打折、積分兌換等。

這種情況,可以引入分散式事務。簡單點的,只需要使用redis的setnx或者zk來控制就可以;複雜點的方案,可以使用二階段提交之類的。

分散式事務的業務粒度,要足夠粗,才能保護這些餘額操作;加鎖的粒度,要足夠細,才能保證系統的效率。

begin transition(userid)
    balance = dao.getBalance(userid)
    balance = balance - cost
    dao.setBalance(userid,balance)
end
複製程式碼

類CAS方式解決

java的朋友可以回想下concurrent包的解決方式。那就是引入了CAS,全稱Compare And Set

擴充套件到分散式環境下,同樣可以採用這一策略。即先比較再設值。如果初始值已經變化了,那麼不允許set設值。

cas一般通過迴圈重試的方法進行狀態更新,但餘額操作一般都是比較單一的,你也可以直接終止操作,並預警風險。

sql類似於:

update user set balance = balance - 20
    where userid=id 
    and balance >= 20
    and balance = $old_balance
複製程式碼

當然,你也可以通過加入版本號概念,而不是餘額欄位來控制這個過程,但都類似。

變種:版本號

通過在表中加一個額外的欄位version,來控制併發。這種方式不去關注餘額,可擴充套件性更強。

version的預設值一般是1,即記錄建立時的預設值。

操作的虛擬碼如下:

version,balance = dao.getBalance(userid)
balance = balance - cost
dao.exec("
    update user 
    set balance = balance - 20
    version = version + 1
    where userid=id 
    and balance >= 20
    and version = $old_version
")
複製程式碼

上面的併發攻擊,將會只有一個操作能夠成功,我們的餘額安全了。

End

趕緊看一下你的餘額操作,是否也暴露在風險之下。你可以選擇接受福報繼續當兄弟,當然也可以將福報留給需要的人。

一念成佛,一念成魔。你才是自己的主人。

併發操作餘額,給996留點福報

相關文章