并发扣款一致性,幂等性问题,这个话题还没聊完!!!
《并发扣款,如何保证数据的一致性?》,分享了同一个用户并发扣款时,有一定概率出现数据不一致,可以使用 CAS 乐观锁的方式,在不降低吞吐量,并且只有少量修改的情况下,保证数据的一致性。
文章发布不到 24 小时,就有近 200 的评论。
其中,问的比较多的是 ABA 问题,这个问题已经在《并发扣款一致性优化,CAS 下 ABA 问题,这个话题还没聊完!!!》中扩展。
其次,问的比较多的是作业题,为什么一定要用 select&set 的方式进行余额写回:
UPDATE t_yue SET money=newmoneyWHEREuid=new_money WHERE uid=newmoneyWHEREuid=uid AND money=$old_money;
为什么不能采用直接扣减的方法:
UPDATE t_yue SET money=money-diffWHEREuid=diff WHERE uid=diffWHEREuid=uid;
很人说,在并发情况下,会将 money 扣成负数。
为了保证余额不被扣成负数,再加一个 where 条件:
UPDATE t_yue SET money=money-diffWHEREuid=diff WHERE uid=diffWHEREuid=uid AND money-$diff>0_;_
这样是否可行?
_画外音:_额,撇开业务不谈,这个 SQL 用列做运算,其实是不好的,建议使用:
UPDATE t_yue SET money=money-diffWHEREuid=diff WHERE uid=diffWHEREuid=uid AND money>$diff_;_
很遗憾,仍然不行。原因在《并发扣款,如何保证数据的一致性?》一文里点赞最多的评论,不幂等。
_画外音:_说明绝大部分同学,能够回答正确作业。
聊幂等性之前,先看另一个测试用例的 case。
假设有一个服务接口,注册新用户:
bool RegisterUser(uid,uid, uid,name){
// 查看 uid 是否已经存在
select uid from t_user where uid=$uid;
// 不是新用户,返回失败
if(rows>0)return false;
else{
// 把新用户插入用户表
insert into t_user values(uid,uid, uid,name);
// 返回成功
return true;
}
}
有一个测试工程师,对该接口写了一个测试用例:
bool TestCase_RegisterUser(){
// 造一些假数据
long uid=123;
String name='shenjian';
// 调用被测试的接口
bool result= RegisterUser(uid,name);
// 预期注册成功,对结果进行断言判断
Assert(result,true);
// 返回测试结果
return result;
}
这是不是一个好的测试用例? 这个用例存在什么问题?
你会发现,相同条件下,这个测试用例执行两次,得到的结果不一样:
(1)第一次执行,第一次造数据,调用接口,注册成功;
(2)第二次执行,又造了一次相同的数据,调用接口,注册会失败;
这不是一个好的测试用例,多次执行结果不同。
什么是幂等性?
相同条件下,执行同一请求,得到的结果相同,才符合幂等性。
画外音:Google 一下,比我解释得更好,但意思应该说清楚了。
如何将上面的测试用例改为符合 “幂等性” 的测试用例呢?
只需要加一行代码:
bool TestCase_RegisterUser(){
// 造一些假数据
long uid=123;
String name=’shenjian’;
// 先删除这个伪造的用户
DeleteUser(uid);
// 调用被测试的接口
bool result= RegisterUser(uid,name);
// 预期注册成功,对结果进行断言判断
Assert(result,true);
// 返回测试结果
return result;
}
这样,在相同条件下,不管这个用例执行多少次,得到的测试结果都是相同的。
是不是对幂等性有点感觉了。
读请求,一般是幂等的。
写请求,视情况而定:
-
insert x,一般来说不是幂等的,重复插入得到的结果不一定一样
-
delete x,一般来说是幂等的,删除多次得到的结果仍相同
-
set a=x 是幂等的
-
set a=a-x 不是幂等的
-
…
因此,这么扣减余额:
UPDATE t_yue SET money=newmoneyWHEREuid=new_money WHERE uid=newmoneyWHEREuid=uid AND money=$old_money;
是幂等操作。
要是这么扣减余额:
UPDATE t_yue SET money=money-diffWHEREuid=diff WHERE uid=diffWHEREuid=uid AND money-$diff>0;
不是幂等操作。
聊到这里,或许有朋友要抬杠了,测试用例会重复执行,扣款怎么会重复执行呢?
重试。
重试,是异常处理里很常见的手段。
你在写业务的时候有没有写过这样的代码:
result = DoSomething();
if(false==result || TIMEOUT){
// 错误,或者超时,重试一次
result= DoSomething();
}
return result;
当然,又会有朋友抬杠了,我从来不重试!!!
画外音:额,这是合格,还是不合格呢?
你可以决定业务代码怎么写,你不能决定底层框架代码怎么写:
(1)站点框架有没有自动重试?
(2)服务框架有没有自动重试?
(3)服务连接池,数据库连接池有没有自动重试?
画外音:
(1)服务化分层的架构中,建议只入口层重试,服务层不要重试,防止雪崩;
(2)dubbo 底层,调用超时是默认重试的,这个设计不好;
因此,在有重试的架构体系里,幂等性是需要考虑的一个问题。
现在该懂了,为啥扣款和充值业务,一般使用:
- select&set,配合 CAS 方案
而不使用:
- set money-=X 方案
画外音:充了 100 电话费,怎么多了 200 块?
知其然,知其所以然,希望大家有收获。
架构师之路 - 分享技术思路
还有很多朋友评论:版本号,CAS 只能在低并发中使用,高并发会有大量的修改失败。或许大家对 “高并发” 有所误解,下一篇聊一聊相关问题。
转载自:https://juejin.cn/post/6920099093259747335