ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

高并发下订单幂等性设计与避坑实践

高并发下订单幂等性设计与避坑实践

1. 问题现象与背景

那天晚上11点半,我正在调试谷粒商城的订单模块,突然发现一个诡异的现象:当用户连续快速点击"提交订单"按钮时,系统偶尔会生成重复订单。这个bug就像定时炸弹一样潜伏在系统中,平时测试时很难复现,但在促销活动期间可能导致严重的资金损失。

作为电商系统,订单模块的核心要求就是"幂等性"——无论用户点击多少次,同一笔交易只能生成一个有效订单。这个原则看似简单,但在高并发场景下要实现却并不容易。我们系统采用的是Spring Cloud微服务架构,订单服务独立部署,通过Feign调用支付服务。

2. 问题排查过程

2.1 初步现象分析

首先我整理了bug出现的规律:

  1. 只在移动端出现(Android/iOS均有报告)
  2. 网络延迟较高时更容易复现
  3. 订单金额越大出现概率越高

通过Charles抓包发现,移动端在弱网环境下会出现请求重试机制。而查看服务端日志时,发现相同orderId的请求确实到达了服务端多次。

2.2 核心问题定位

深入分析订单服务代码后发现问题出在防重校验逻辑:

// 原错误代码示例 public Order createOrder(OrderDTO orderDTO) { // 查询是否已存在订单(问题点1:非原子操作) Order existOrder = orderMapper.selectByOrderNo(orderDTO.getOrderNo()); if(existOrder != null) { return existOrder; } // 生成订单(问题点2:无事务隔离) Order newOrder = generateOrder(orderDTO); orderMapper.insert(newOrder); // 调用支付(问题点3:耗时操作) paymentFeign.createPayment(newOrder); return newOrder; }

这段代码存在三个致命缺陷:

  1. 查询和插入不是原子操作,存在并发间隙
  2. 没有使用数据库事务隔离
  3. 支付调用是网络IO操作,会延长临界区时间

3. 解决方案设计与实现

3.1 分布式锁方案

第一版修复方案采用Redis分布式锁:

public Order createOrderWithLock(OrderDTO orderDTO) { String lockKey = "order:lock:" + orderDTO.getOrderNo(); try { // 获取分布式锁(设置3秒超时) boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS); if(!locked) { throw new BusinessException("操作太频繁,请稍后重试"); } // 原有业务逻辑 return doCreateOrder(orderDTO); } finally { // 释放锁 redisTemplate.delete(lockKey); } }

这个方案上线后解决了大部分重复订单问题,但在大促期间出现了新问题:

  • Redis集群网络抖动导致锁失效
  • 业务处理超时导致锁提前释放
  • 锁等待导致接口响应变慢

3.2 最终解决方案:数据库唯一索引+乐观锁

经过多次优化,最终采用的方案是:

  1. 数据库层增加唯一索引:
ALTER TABLE t_order ADD UNIQUE INDEX uk_order_no (order_no);
  1. 业务代码改造:
@Transactional public Order createOrderFinal(OrderDTO orderDTO) { // 先尝试插入(利用数据库唯一约束) Order newOrder = generateOrder(orderDTO); try { orderMapper.insert(newOrder); } catch (DuplicateKeyException e) { // 如果已存在则直接返回 return orderMapper.selectByOrderNo(orderDTO.getOrderNo()); } // 异步调用支付(通过消息队列) rabbitTemplate.convertAndSend( "order.payment.exchange", "payment.create", new PaymentMessage(newOrder.getId())); return newOrder; }

这个方案的优势在于:

  • 利用数据库唯一约束保证原子性
  • 事务范围最小化
  • 耗时操作异步化处理
  • 无额外中间件依赖

4. 经验总结与避坑指南

4.1 关键经验

  1. 幂等性设计三原则:

    • 前端:按钮防重复点击(禁用+倒计时)
    • 网关层:请求去重(相同orderNo 5秒内只放行一次)
    • 服务层:最终防线的原子校验
  2. 性能与安全的平衡:

    • 分布式锁适用于低频敏感操作
    • 数据库约束适用于高频核心业务
    • 异步化处理能显著提升吞吐量
  3. 监控指标:

    // 使用Micrometer监控重复订单率 Metrics.counter("order.duplicate.count") .tag("source", "mobile") .increment();

4.2 典型避坑场景

  1. 测试环境复现技巧:

    • 使用Postman的Runner功能并发发送请求
    • 在Kubernetes中模拟网络延迟:
      kubectl exec -it <pod> -- tc qdisc add dev eth0 root netem delay 500ms
  2. 常见错误模式:

    • 只依赖前端防重复点击(不可靠)
    • 在事务外部做校验(仍有并发间隙)
    • 使用synchronized代替分布式锁(集群环境失效)
  3. 应急处理方案:

    -- 查询重复订单 SELECT order_no, COUNT(*) FROM t_order GROUP BY order_no HAVING COUNT(*) > 1; -- 合并重复订单(保留最早创建的) DELETE FROM t_order WHERE id NOT IN ( SELECT MIN(id) FROM t_order GROUP BY order_no );

这次bug修复让我深刻体会到,在高并发系统中,任何非原子操作都可能成为定时炸弹。最终的解决方案虽然看起来简单,但需要全面考虑性能、可靠性和维护成本三个维度。

返回列表