老生常谈的问题:电商业务中如何防止重复下单?

用户下单流程我们从用户浏览商品开始 , 看看用户下单的简要过程:

老生常谈的问题:电商业务中如何防止重复下单?

文章插图
用户下单简要过程
  • 浏览商品:用户查看商品详情
  • 加购/结算:用户可以选择直接购买商品 , 也可以先加入购物车 , 用户购买的这一步就是结算
  • 确认下单:结算完成 , 就进入了下单页面 , 提交订单 , 这一步就会生成一个订单 , 然后进入付款页面
我们可以看到 , 下单是发生在结算之后 , 下单之后 , 会生成唯一的订单号 , 接下来 , 客户端需要用这个订单号去完成支付 。
那接下来先看看 , 为什么发生重复下单?
为什么会重复下单为什么会重复下单 , 对于订单服务而言 , 就是接到了多个下单的请求 , 原因可能有很多 , 最常见的是这两种:
  • 用户重复提交
  • 网络原因导致的超时重试

老生常谈的问题:电商业务中如何防止重复下单?

文章插图
重复下单原因
如何防止重复下单防止用户提交 , 最常规的做法 , 就是客户端点击下单之后 , 在收到服务端响应之前 , 按钮置灰 。
当然 , 防止重复下单 , 肯定不能只依靠客户端 , 可能会因为一些网络的抖动 , 导致仍然有重复的请求到达服务端 , 所以还是要在服务端做防重/幂等的处理 。
PS:这里额外插入一点我对防重和幂等的理解:防重指的是防止重复提交 , 幂等指的是多次请求如一次 , 简单说 , 就是防重可以给对重复请求抛异常 , 幂等是对重复的请求响应第一次的结果 , 在我们讨论的这个场景里 , 幂等就是响应唯一的订单号 。
老生常谈的问题:电商业务中如何防止重复下单?

文章插图
防重和幂等
防重第一步 , 需要识别请求是否重复 , 这一步 , 需要客户端配合实现 。
为什么呢?大家想一下 , 下单的时候 , 服务端怎么去判断这个下单请求是否唯一呢?金额?商品?优惠券?……万一用户就是喜欢 , 又下了一个一模一样的单呢?
所以 , 需要客户端在请求下单接口的时候 , 需要生成一个唯一的请求号:requestId , 服务端拿这个请求号 , 判断是否重复请求 。
那么 , 接下来 , 压力就给到服务端了 , 看看服务端怎么实现防重/幂等吧!
利用数据库实现幂等可以在订单表t_order里添加一个字段:requestId , 添加唯一索引:
老生常谈的问题:电商业务中如何防止重复下单?

文章插图
唯一请求字段
这样一来 , 如果是重复的请求 , 在落库的时候就会报错 , 为了保证幂等性 , 我们可以catch住这个异常 , 根据requestId获取订单号 , 然后向客户端响应订单号 。
大概的代码如下:
PlaceOrderResVO placeOrder(PlaceOrderReqVO reqVO) {try {//下单业务逻辑……//生成订单号String oid=generateOid();……//订单落库Order order = orderMApper.saveOrder(orderDO);//响应订单resVO.setOid(order.getOid());return resVO;} catch(UniqueKeyViolationException e) {// 发生了重复异常// 根据请求号获取订单Order order = getOrderByRequestId(reqVO.getRequestId());resVO.setOid(order.getOid());return resVO;} catch (Exception e) {}}当然 , 这里不太好的地方是 , 拿异常来做业务判断 。
利用redis防重另外一个办法 , 就是下单请求的时候要加锁了 , 通常我们的服务都是集群部署 , 所以一般都是用Redis实现分布式锁 。


推荐阅读