编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

接口幂等的3层面解决方案,3个业务场景,4个问题根源及2个操作

wxchong 2024-07-21 07:04:32 开源技术 7 ℃ 0 评论

接口幂等设计

背景

那天下班前,突然群里说你们系统有bug, 你们的打款按钮怎么点了没反应啊? ,没过一会群里又有人反应,给用户重复打款了!,这消息一出群里立马炸锅了,跟钱相关绝非小事,通知相关人员,经排查可知,运营人员点了没反应又点了一次,谁知道后端开发竟然没有重复校验再次打款,给公司造成了很大经济损失,由此可见,我们开发时有些业务一定要做幂等处理。

幂等概念

幂等就是不管你执行多少遍,结果都是一样的。用函数表达就是f(f(x))=f(x)。

幂等问题4种根源

  • 重复点击
  • 恶意狂刷
  • 失败重试(超时、异常)
  • 重复消息(重复发送,重复消费)

需考虑幂等的3种业务场景

  • 钱相关的业务(例如:抢红包,订单支付/退款,理财购买/赎回,转账等等)
  • 类似钱相关的业务(例如:积分发放/使用,能量领取/使用,某某币发放/扣减等等)
  • 库存相关的业务(例如:秒杀,抢购,抽奖等等)

需考虑幂等的2种操作

其实我们对业务进行深入的思考,可以把业务操作抽象为这4种操作:增、删、改和查,就是平时所说的CRUD。不管进行多少次的查和删结果都一样,可见查和删是天然的幂等,而需要我们处理的就是增和改这2种操作了。

3个层面的幂等解决方案

web层

按钮置灰/load状态

跳转结果页

  • 应用层

token机制

token机制可分两步,第一步:客户端在调业务接口前,先从服务端获取token令牌,服务端把token令牌存入redis;第二步:客户端携带token调用业务接口,一般会放入请求头header中,服务端拿到token后,去redis执行删除操作,删除成功,继续后面业务处理;删除失败,则返回重复执行。如下图所示:



缺点:每次业务请求,都需要一次额外的请求。比如:在真实环境中,1W个请求有10个重复,而我们需要为了这10个请求,需要付出9990个额外请求,值吗? 个人认为:这个方案在实际工作应该用得最少。要想用的话,可以用在更新操作中,token可以随着详情接口一起下发,来减少一次获取token请求。

基于redis实现(set NX PX)

具体流程:服务端接收到请求后,根据请求参数确定唯一key,通过set NX PX命令写到redis中,写入成功,则执行下面业务逻辑,写入失败,则返回重复执行。

确定唯一key的方法如下:

1) 可以用请求参数中某一个或几个来确定唯一key

2)md5(所有请求参数)来确定唯一key

3)用户id+方法名来确定唯一key

去重表(记录表)

防重表通常是指各种记录表,如:订单记录表,支付记录表,退款记录表,积分记录表等等。流程:查询防重表是否存在记录,如果不存在,则执行下面业务逻辑,否则,返回重复执行。

状态机

很多业务会一个业务流转状态,每个状态都会有前置状态和后置状态。以订单为例,已支付的前置状态只能是待支付,而取消状态的前置状态只能是待支付,因此,可以通过这种状态机的流转来控制请求的幂等。

  • 数据库层

新增操作: 唯一索引

更新操作:乐观锁

幂等总结

实际工作中,用得比较多就是基于redis实现的,最后数据库做兜底策略

幂等实战

模拟恶意狂刷退款接口,能否成功拦截多次打款情况。

public BaseResponse<BoolResult> refund(RefundReq refundReq) {

    String orderNo = refundReq.getOrderNo();

    // 基于redis实现幂等
    Boolean result = redisClient.setNx("refund:" + orderNo,orderNo, 5000);
    if (!result) {
        throw new RuntimeException("重复执行!");
    }

    OrderInfo orderInfo = baseMapper.selectOne(new LambdaQueryWrapper<OrderInfo>().eq(OrderInfo::getOrderNo,orderNo));
    if (orderInfo == null) {
        throw new RuntimeException("订单不存在!");
    }

    if (1 != orderInfo.getStatus().intValue()) {
        throw new RuntimeException("只有支付订单才能退款!");
    }

    if (0 != orderInfo.getRefundStatus().intValue()) {
        throw new RuntimeException("只有未退款的订单才能退款!");
    }

    RefundInfo refundInfo = new RefundInfo();
    refundInfo.setOrderNo(orderNo);
    refundInfo.setStatus(0);
    refundInfo.setAmount(orderInfo.getAmount());
    refundInfoMapper.insert(refundInfo);
    return BaseResponse.ok(new BoolResult(true));
}

结果如下:



Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表