什么是幂等设计?
在编程场景指的是:使用相同参数来调用同一接口,调用多次的结果跟单次产生的结果是一致的。
企业级项目的一些关键接口都需要幂等设计,比如支付扣款、发货等等。
设想以下,因为网络问题我们调用扣款接口超时了,并且没有进行重试,这样有可能给用户发货了,但是实际没扣款。因此这种情况下通常要重试扣款。但是如果重试了,假设之前超时的那次调用实际是成功了,只是响应结果的时候接口超时了,这样不是重复扣两次款了?
如果你是那个用户,买一个东西,平台竟然扣了两次钱,你肯定会抓狂并且投诉。
那如果我们是平台会怎么样?一直被用户投诉,渐渐的流失用户直到倒闭。
所以幂等设计在一些必须要保证业务一致性的情况下,非常关键,因为这种场景往往需要重试,重试就需要幂等。
当然,还有一些情况是用户误触的,比如多次点击按钮导致多次提交等。
需要注意,虽然前端可以通过将按钮置为灰防止重复点击,但是纯前端无法完美实现幂等性!比如前端调用后端接口超时,有可能后端已经存储了数据,此时前端的按钮已经可点击,用户再次点击会生成两条数据。
实现幂等性的方案选型
1、数据库唯一索引
利用数据库唯一索引的一致性保证幂等性。
比如将数据库订单表中的订单号字段配置成唯一索引,用户生成订单会执行insert语句,MySQL根据唯一所以天然阻止相同订单号数据的插入,我们可以catch住报错,让接口正常返回插入成功结果。
try{
insertOrder({id:xxx});
}catch(DuplicateKeyException e){
return true;
}
对应的订单插入语句为:
insert into order(id,order,xx,updateTime) values(1,2,3,"2024-07-24 15:55:18")
on dupicate key update updateTime = now();
这样同一笔订单,不论调用几次,结果都不会新增重复的订单记录。
2、数据库乐观锁
利用乐观锁在某些场景下也能实现幂等性。
比如需要对一个配置进行修改,同时记录修改的时间、旧配置、新配置、操作人等日志信息到操作记录表中,方便后面追溯。
// update sys_config set config = "a" where id = 1;
updateConfig();
// insert opreation_log(createTime, oldConfig, newConfig, userId)
// value("2024-05-28 15:55:18","b","a",1L)
addOpreationLog();
这个场景就很适合采用乐观锁来实现幂等。
乐观锁并不是真的加锁,而是可以给配置表加一个version版本号字段,每次修改需要验证版本号是否等于修改前的(没被别人同时修改),然后才能给版本号加1。
如果配置表修改成功(通过影响行数来判断1表示成功,0表示失败,才能添加操作日志。
因此,进行如下改造:
// update sys_config set config = "a" where id = 1;
int updateEffect = updateConfig();
// insert opreation_log(createTime, oldConfig, newConfig, userId)
// value("2024-05-28 15:55:18","b","a",1L)
if(updateEffect == 1){
addOpreationLog();
}
3、天然幂等操作
比如一些delete操作,这种是天然幂等的,因为删除一次和多次都是一样的。
还有一些更新操作,例如:
update sys_config set conifg = "a" where id = 1;
这样的SQL不论执行几遍,结果都是一样的。
如果接口里面仅包含上述的这些天然幂等的行为,那么对外就可以标记当前接口为幂等接口,不需要任何其他操作。
4、分布式锁
导致数据错乱的元凶很多时候都是”并发修改“。
很多时候的业务场景是这样的:
1、查找数据
2、if(不包含这个数据){
3、插入这条数据
}
在没有并发的情况下,这样的逻辑没任何问题,但是一旦出现并发,就会导致数据不一致的情况。
因为同时可能出现多个线程在同一时刻到达第2步的判断,这时候其实数据都没有插入,因此它们都能通过这个判断到达第3步,这就导致重复插入一样的数据。
针对这种场景,可以上一把分布式锁,杜绝了并发问题。
分布式锁{
1、查找数据
2、if(不包含这个数据){
3、插入这条数据
}
}
多个线程需要先抢占锁才能进行后续的业务操作,因此1、2、3这三个步骤在同一时刻仅会有一个线程执行,所以不会存在数据不一致的情况。
也因此加了锁之后,这个业务代码可以进行多次调用,因为除了第一次的调用,后续通过if判断,都不会插入数据。