公众号merlinsea
- 背景
- 用户下单的时候,可能由于网络延迟导致用户多次提交的是同一笔订单,但作为业务方需要保证用户只会支付一次。即用户多次点击提交支付的时候下单接口应该具有幂等性。
- 幂等性介绍
- 幂等即如果用户同一个请求多次到达,那么我们只能处理一次,即默认第一次会处理,后续的请求会忽略。
- 对于输出,同一个请求不论来多少次,返回的结果都必须是相同的。
- 因此幂等通常会选取请求中可以唯一标识这个请求的参数作为幂等key,幂等请求的结果和幂等key应该存储在redis中,后序相同的请求到来可以直接冲redis中获取。
- 用户下单流程
- 1、选中购物车中的商品,点击结算 ------->后端返回一个提交令牌token同时跳转到提交订单支付页面【提交订单】
- 2、用户点击提交订单,进行支付【确认支付】
- 说明:下单流程中第1步提交订单实际上是直接和我们的后台进行响应,速度是比较快的,因此在这一步是不会出现重复下单,但对于第2步由于支付需要和第三方交互,用户可能由于延迟多次支付,因此在第1步的时候我们会生成一个唯一标识这个订单的令牌token交给用户【同时token会在后端保存一份】,用户在第2步的时候会携带token进行支付【支付完后端就删除了这个token】,这样我们就可以保证不论用户支付多少次,实际上的付款只会执行一次。
- 解决重复下单问题的思路
- 将用户下单过程分为两步执行,第一步中会生成一个提交令牌token,并将这个提交的令牌token存到redis中,在第二步中用户点击提交订单进行支付的时候会携带上这个提交令牌token,如果发现redis中有这个token说明是第一次提交,调用第三方支付接口同时删除redis中的这个令牌。这样在第二个步骤中不管用户点击了多少次都只会支付一次(因为后续的提交都找不到这个token了)。
- 第一步:点击购物车结算按钮,获取提交订单的唯一令牌【这步是和本项目的微服务交互,故速度会比较快】
@ApiOperation("获取提交订单令牌") @GetMapping("get_token") public JsonData getOrderToken(){ LoginUser loginUser = LoginInterceptor.threadLocal.get(); String key = String.format(CacheKey.SUBMIT_ORDER_TOKEN_KEY,loginUser.getId()); String token = CommonUtil.getStringNumRandom(32); redisTemplate.opsForValue().set(key,token,30,TimeUnit.MINUTES); return JsonData.buildSuccess(token); }
- 第二步:点击提交订单接口并携带token【这一步要和第三方支付交互,速度可能会比较慢,关键是在这步容易出现重复提交】
/** * * 防重提交 * * 用户微服务-确认收货地址 * * 商品微服务-获取最新购物项和价格 * * 订单验价 * * 优惠券微服务-获取优惠券 * * 验证价格 * * 锁定优惠券 * * 锁定商品库存 * * 创建订单对象 * * 创建子订单对象 * * 发送延迟消息-用于自动关单 * * 创建支付信息-对接三方支付 * * @param orderRequest * @return */ @Override @Transactional public JsonData confirmOrder(ConfirmOrderRequest orderRequest) { LoginUser loginUser = LoginInterceptor.threadLocal.get(); String orderToken = orderRequest.getToken(); if(StringUtils.isBlank(orderToken)){ throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_NOT_EXIST); } //原子操作 校验令牌,删除令牌 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<>(script,Long.class), Arrays.asList(String.format(CacheKey.SUBMIT_ORDER_TOKEN_KEY,loginUser.getId())),orderToken); //redis中找不到提交token 说明是重复提交,直接抛出异常 if(result == 0L){ throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL); } //说明是第一次提交,执行下单支付相关业务todo【调用第三方支付接口】 }