接口幂等性-订单提交

目录

一、什么是幂等性

二、哪些情况需要防止

三、什么情况下需要幂等

四、幂等解决方案

1、token 机制

2、各种锁机制

(1)数据库悲观锁

(2)数据库乐观锁

(3)业务层分布式锁

3、各种唯一约束

(1)数据库唯一约束

(2)redis set 防重

4、防重表

5、全局请求唯一 id

案例:订单提交


一、什么是幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的 ,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,  这就没有保证接口的幂等性。

二、哪些情况需要防止

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
  • 其他业务情况

三、什么情况下需要幂等

SQL 为例,有些操作是天然幂等的。
SELECT * FROM table WHER id=? ,无论执行多少次都不会改变状态,是天然的 幂等
UPDATE tab1 SET col1=1 WHERE col2=2 ,无论执行成功多少次状态都是一致的,也是 幂等 操作。
delete from user where userid=1 ,多次操作,结果一样,具备 幂等
insert into user(userid,name) values(1,'a') userid 为唯一主键,即重复操作上面的业务,只 会插入一条用户数据,具备幂等 性。
UPDATE tab1 SET col1=col1+1 WHERE col2=2 ,每次执行的结果都会发生变化,不是 幂等 的。
insert into user(userid,name) values(1,'a') userid 不是主键,可以重复,那上面业务多次操 作,数据都会新增多条,不具备幂等 性。

四、幂等解决方案

1token 机制

  1. 服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
  2. 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  3. 服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
  4. 如果判断 token 不存在 redis ,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。

 接下来考虑提交订单后是先删除token还是先执行业务呢?

分析:

后删除token

① 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两遍

② 执行业务期间,因为token还未删除,此时如果顾客按了两次提交订单,那么就导致执行两次了。

先删除token

① 代码格式大概如下

if(serverToken == redisToken) {

    del(serverToken)
    执行业务
}

② 如果考虑极限一点,那么仍然可能出现客户连续点击提交订单,同时过判断,执行多次业务

但是显而易见的是第二种的可能性比较小,危险性较低,所以选择先删除Token,接下来考虑优化

我们可以将判断token是否存在redis中和删除redis中token这三个步骤捆绑在一起,也就是Token 获取、比较和删除必须是原子性

(1) redis.get(token) token.equals redis.del(token) 如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行
(2) 可以在 redis 使用 lua 脚本完成这个操作
if 
    redis.call('get', KEYS[1]) == ARGV[1] 
then 
    return redis.call('del', KEYS[1]) 
else 
    return 0 
end

2、各种锁机制

(1)数据库悲观锁

select * from xxxx where id = 1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。

(2)数据库乐观锁

这种方法适合在更新的场景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 1 ,调用库存服务version 变成了 2 ;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订单服务传如的 version 还是 1 ,再执行上面的 sql 语句时,就不会执行;因为 version 已经变为 2 了, where 条件 就不成立。这样就保证了不管调用几次,只会真正的处理一次。乐观锁主要使用于处理读多写少的问题

(3)业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过

3、各种唯一约束

(1)数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

(2)redis set 防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis set ,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。

4、防重表

使用订单号 orderNo 作为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的 redis 防重也算’

5、全局请求唯一 id

调用接口时,生成一个唯一 id redis 将数据保存到集合中(去重),存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;

案例:订单提交

这里在进入订单确认页的时候我们就给客户端发送一个token并存放进redis,然后在提交订单的时候携带token发送到服务端,服务端从redis中取出token和客户端发送过来的进行验证,如果一样就可以创建订单。当然这里从redis中取出token判断是否一致、和删除token这三个操作需要原子性。

① 在页面代码中携带订单应该提交的相关信息
    <form action="http://order.gulimall.com/submitOrder" method="post">
        <input type="hidden" name="addrId" id="addrIdInput"/>
        <input type="hidden" name="payPrice" id="payPriceInput"/>
        <input type="hidden" name="orderToken" th:value="${orderConfirmData.Token}"/>
        <button class="tijiao" type="submit">提交订单</button>
    </form>

② 设置值

    function getFare(addrId) {
        $("#addrIdInput").val(addrId)
        $.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" + addrId, function (data) {
            console.log(data);
            $("#fare").text(data.data.fare)
            var total = [[${orderConfirmData.total}]]
            // 设置应付价格
            var payPrice = data.data.fare * 1 + total * 1
            $("#payPriceEle").text(payPrice)
            $("#payPriceInput").val(payPrice)
            $("#recieveAddressEle").text(data.data.address.provice + "" + data.data.address.detailAddress);
            $("#recieverEle").text(data.data.address.name)
        })
    }

③ 首先在数据库层次保证订单的唯一性,为订单编号加上唯一性约束

④ 后端订单接口

    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo vo) {
        // 下单,创建订单,校验令牌,检验价格,锁库存
        SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
        System.out.println("订单提价的数据。。。" + vo);

        if(responseVo.getCode() == 0) {
            // 成功显示支付页面
            return "pay";
        } else {
            // 失败返回订单确认页面
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }

⑤ 保证从redis获取token和比较、删除token是原子性的

    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

        submitVoThreadLocal.set(vo);
        MemberResVo memberResVo = LoginUserInterceptor.loginUser.get();

        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
        String orderToken = vo.getOrderToken();
        // 成功返回1  失败返回0
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // 保证原子性
        Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId()), orderToken);
        if(result == 0L) {
            // 验证失败
            response.setCode(1);
            return response;
        } else {
            // 下单,创建订单,校验令牌,检验价格,锁库存
            // 1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            // 2、验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            if(Math.abs(payAmount.subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
                // 金额对比成功后保存订单
                saveOrder(order);
            } else {
                response.setCode(2); // 金额对比失败
                return response;
            }

        }

        // 逻辑如下,但是其实我们需要保证对比和删除是原子性的,因此使用redisTemplate的脚本
//        if(orderToken != null && orderToken.equals(redisToken) ) {
//            // 检验成功
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResVo.getId());
//        } else {
//            // 失败
//        }
        response.setCode(1);
        return response;
    }

  • 49
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 40
    评论
分布式接口幂等性问题是指在分布式系统中,由于网络延迟、重试机制等原因,可能导致同一个请求被重复处理,从而产生重复的业务逻辑。为了解决这个问题,需要保证接口幂等性。 保证接口幂等性的方法有多种。一种常见的方法是使用唯一标识来标识每一次请求,比如订单id、支付流水号或者前端生成的唯一随机串。在每次请求之前,需要将唯一标识存放到数据库或者缓存中。后端服务在处理请求之前,需要先检查这个唯一标识是否存在,如果存在,则判定此次请求已经处理过,不需要进行重复处理。这样可以避免重复的业务逻辑。 在分布式场景中,由于负载均衡算法的原因,可能会导致同一个请求被多台机器处理。为了解决这个问题,可以使用分布式锁来保证只有一个机器能够处理该请求。另外,使用分布式事务也可以保证接口幂等性。 此外,还可以通过拦截器(AOP)和注解的方式实现一个通用的解决方案,避免每次请求都写重复的代码。在设计系统时,幂等性是一个需要首要考虑的问题,特别是在涉及到金融交易等关键业务的系统中。 综上所述,保证分布式接口幂等性可以通过使用唯一标识、分布式锁、分布式事务等方法来实现。这样可以避免重复的业务逻辑和数据不一致的问题。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [分布式环境下接口幂等性浅析](https://blog.csdn.net/ice24for/article/details/86084613)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [分布式开发(二)---接口幂等性(防止重复提交)](https://blog.csdn.net/icanlove/article/details/117652662)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zoeil

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值