1. 问题现象与背景
那天晚上11点半,我正在调试谷粒商城的订单模块,突然发现一个诡异的现象:当用户连续快速点击"提交订单"按钮时,系统偶尔会生成重复订单。这个bug就像定时炸弹一样潜伏在系统中,平时测试时很难复现,但在促销活动期间可能导致严重的资金损失。
作为电商系统,订单模块的核心要求就是"幂等性"——无论用户点击多少次,同一笔交易只能生成一个有效订单。这个原则看似简单,但在高并发场景下要实现却并不容易。我们系统采用的是Spring Cloud微服务架构,订单服务独立部署,通过Feign调用支付服务。
2. 问题排查过程
2.1 初步现象分析
首先我整理了bug出现的规律:
- 只在移动端出现(Android/iOS均有报告)
- 网络延迟较高时更容易复现
- 订单金额越大出现概率越高
通过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; }这段代码存在三个致命缺陷:
- 查询和插入不是原子操作,存在并发间隙
- 没有使用数据库事务隔离
- 支付调用是网络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 最终解决方案:数据库唯一索引+乐观锁
经过多次优化,最终采用的方案是:
- 数据库层增加唯一索引:
ALTER TABLE t_order ADD UNIQUE INDEX uk_order_no (order_no);- 业务代码改造:
@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 关键经验
幂等性设计三原则:
- 前端:按钮防重复点击(禁用+倒计时)
- 网关层:请求去重(相同orderNo 5秒内只放行一次)
- 服务层:最终防线的原子校验
性能与安全的平衡:
- 分布式锁适用于低频敏感操作
- 数据库约束适用于高频核心业务
- 异步化处理能显著提升吞吐量
监控指标:
// 使用Micrometer监控重复订单率 Metrics.counter("order.duplicate.count") .tag("source", "mobile") .increment();
4.2 典型避坑场景
测试环境复现技巧:
- 使用Postman的Runner功能并发发送请求
- 在Kubernetes中模拟网络延迟:
kubectl exec -it <pod> -- tc qdisc add dev eth0 root netem delay 500ms
常见错误模式:
- 只依赖前端防重复点击(不可靠)
- 在事务外部做校验(仍有并发间隙)
- 使用synchronized代替分布式锁(集群环境失效)
应急处理方案:
-- 查询重复订单 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修复让我深刻体会到,在高并发系统中,任何非原子操作都可能成为定时炸弹。最终的解决方案虽然看起来简单,但需要全面考虑性能、可靠性和维护成本三个维度。