《架构师之路:架构设计中的100个知识点》
115.幂等性
有朋友问我:
沈老师,我们有个业务,同一个用户在并发“查询,逻辑计算,扣款”的情况下,余额可能出现不一致,请问有什么优化方法么?
今天和大家聊一聊这个问题。
画外音:文章较长,建议提前收藏。
问题一:用户扣款的业务场景是怎样的?
用户购买商品的过程中,要对余额进行查询与修改,大致的业务流程如下:
第一步,从数据库查询用户现有余额:
SELECT money FROM t_yue WHERE uid=$uid;
不妨设查询出来的$old_money=100元。
1. 先查询购买商品的价格,例如是80元;
2. 再查询产品是否有活动,以及活动折扣,例如是9折;
3. 比对余额是否足够,足够时才往下走;
$new_money=$old_money-80*0.9=28
} else {
return "Not enough minerals";
}
第三步,将数据库中的余额进行修改。
UPDATE t_yue SET money=$new_money
WHERE uid=$uid;
极限情况下,可能出现这样的异常流程:

步骤二,业务1和业务2并发进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元。
画外音:假设业务1先写回余额,业务2再写回余额。
这种悲观锁方案确实可行,但要引入额外的组件(redis/zk),并且会降低吞吐量。
对于小概率的不一致,有没有乐观锁的方案呢?
而业务1并发写回时,旧余额确实是100,理应写回成功。
可实际上,这个时候数据库中的金额已经变为28了,所以业务2的并发写回,不应该成功。
具体到这个case,只需要将:
UPDATE t_yue SET money=$new_money
WHERE uid=$uid;
升级为:
UPDATE t_yue SET money=$new_money
WHERE uid=$uid AND money=$old_money;
即可。
并发操作发生时:
业务1执行:
UPDATE t_yue SET money=28
WHERE uid=$uid AND money=100;
业务2执行:
UPDATE t_yue SET money=38
WHERE uid=$uid AND money=100;
优化不难,只改了半行SQL,但确实能解决问题。
UPDATE t_yue SET money=money-$diff
WHERE uid=$uid;
bool RegisterUser($uid, $name){
//查看uid是否已经存在
select uid from t_user where uid=$uid;
//不是新用户,返回失败
if(rows>0)return false;
else{
bool TestCase_RegisterUser(){
//造一些假数据
long uid=123;
String name='shenjian';
//调用被测试的接口
bool result= RegisterUser(uid,name);
//预期注册成功,对结果进行断言判断
Assert(result,true);
//返回测试结果
return result;
}
bool TestCase_RegisterUser(){
//造一些假数据
long uid=123;
String name=’shenjian’;
//先删除这个伪造的用户
DeleteUser(uid);
//调用被测试的接口
bool result= RegisterUser(uid,name);
//预期注册成功,对结果进行断言判断
Assert(result,true);
//返回测试结果
return result;
}
读请求,一般是幂等的。
写请求,视情况而定:
UPDATE t_yue SET money=$new_money
WHERE uid=$uid AND money=$old_money;
UPDATE t_yue SET money=money-$diff
WHERE uid=$uid AND money-$diff>0;
-
select&set,配合CAS方案
-
set money-=X方案

