真的很危險,有人所以進了局子;也有公司所以損失上億。java
想象一下你在一個月黑風高的夜晚,大概是10點多鐘的樣子,加班歸來,打算到小賣部弄盒煙抽。 夜涼風急,你用力裹了下被風鼓起的外套。 那裏有你暗戀的收銀姑娘。 沒日沒夜的工做,只有這十幾分鍾,能讓你感到些許生活的意義。 從羞澀的錢包裏翻出僅存的一張百元大鈔,結帳。而後用顫抖的雙手接過收銀員的找零。 不是由於輕觸到了她的指尖。 也並不是因她如花的笑靨。redis
只由於,腦海裏居然不爭氣的浮現出這樣的過程。sql
balance = dao.getBalance(userid)
balance = balance - cost
dao.setBalance(userid,balance)
複製代碼
還真是狗改不了吃屎啊,果真仍是一個碼畜。提醒着本身,自卑的埋下了臉,快步走開。安全
這是什麼?這是咱們送給996公司的一點福報。bash
餘額修改,是交易系統裏最多見的操做。上面的僞代碼,大意是先取出餘額,而後扣掉消費,而後再回寫餘額。一般狀況下這不會發生問題。併發
除非是高併發,與你是否單機無關。分佈式
對單一餘額的高併發操做天然不是正常人發起的,系統正在承受攻擊,或者自覺得是的使用了MQ。在攻擊面前,上面的操做顯得不堪一擊。高併發
拿一個最嚴重的例子說明:同時發起了一筆消費20元和消費5元的請求。在通過一波猛如虎的操做以後,兩個請求都支付成功了,但只扣除了5元。至關於花了5塊錢,買了25的東西。 ui
劃重點:把以上操做擴展到提現操做上,就更加的恐怖。好比你發起了一筆100元的提現和0.01元的提現,結果餘額被扣減0.01元的提現給覆蓋了。這至關於你有了一個提款機,非要薅到平臺倒閉爲止。防禦
辦法update user set balance = balance - 20
where userid=id
複製代碼
這條語句就保險了不少,若是考慮到餘額不能爲負的狀況,能夠把sql更加精進一點。spa
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
複製代碼
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 ")
複製代碼
上面的併發攻擊,將會只有一個操做可以成功,咱們的餘額安全了。
趕忙看一下你的餘額操做,是否也暴露在風險之下。你能夠選擇接受福報繼續當兄弟,固然也能夠將福報還給資本家。
一念成佛,一念成魔。你纔是本身的主人。