30

余额,危险的操作,给996留点福报

 5 years ago
source link: http://www.sayhiai.com/index.php/archives/115/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

真的很危险,有人因此进了局子;也有公司因此损失上亿。

想象一下你在一个月黑风高的夜晚,大概是10点多钟的样子,加班归来,打算到小卖部弄盒烟抽。

夜凉风急,你用力裹了下被风鼓起的外套。

那里有你暗恋的收银姑娘。

没日没夜的工作,只有这十几分钟,能让你感到些许生活的意义。

从羞涩的钱包里翻出仅存的一张百元大钞,结账。然后用颤抖的双手接过收银员的找零。

不是因为轻触到了她的指尖。

也并非因她如花的笑靥。

只因为,脑海里竟然不争气的浮现出这样的过程。

balance = dao.getBalance(userid)
balance = balance - cost
dao.setBalance(userid,balance)

还真是狗改不了吃屎啊,果然还是一个码畜。提醒着自己,自卑的埋下了脸,快步走开。

这是什么?这是我们送给996公司的一点福报。

一波麻6的操作

余额修改,是交易系统里最常见的操作。上面的伪代码,大意是先取出余额,然后扣掉消费,然后再回写余额。通常情况下这不会发生问题。

除非是高并发,与你是否单机无关。

对单一余额的高并发操作自然不是正常人发起的,系统正在承受攻击,或者自以为是的使用了MQ。在攻击面前,上面的操作显得不堪一击。

拿一个最严重的例子说明:同时发起了一笔消费20元和消费5元的请求。在经过一波猛如虎的操作之后,两个请求都支付成功了,但只扣除了5元。相当于花了5块钱,买了25的东西。

VjuYr2Y.png!web

划重点:把以上操作扩展到提现操作上,就更加的恐怖。比如你发起了一笔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
")

上面的并发攻击,将会只有一个操作能够成功,我们的余额安全了。

赶紧看一下你的余额操作,是否也暴露在风险之下。你可以选择接受福报继续当兄弟,当然也可以将福报还给资本家。

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

FRbmmq7.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK