1. 充电订单防重复的核心挑战与解决思路在充电桩管理平台的实际运营中订单重复问题一直是困扰开发者的痛点。想象这样一个场景用户A在APP上点击开始充电按钮由于网络延迟没有立即收到响应于是又快速点击了两次。如果系统不做任何防护可能会产生三个相同的充电订单导致用户被多次扣费或设备异常启动。这种重复请求问题在分布式环境下尤为突出主要源于以下几个技术难点网络不确定性移动端与服务器之间的网络波动可能导致请求重传用户操作习惯用户习惯性重复点击以确认操作成功分布式系统特性多节点部署时请求可能被不同服务实例处理要解决这个问题我们需要从两个维度入手技术维度前端防抖通过JavaScript限制按钮在短时间内只能提交一次后端幂等保证同一操作多次执行结果与一次执行相同分布式锁防止多节点同时处理相同请求业务维度订单状态机明确定义订单状态流转规则唯一性约束利用数据库唯一索引防止重复记录请求指纹通过用户设备时间等组合生成唯一请求ID2. 幂等性设计的实现原理2.1 幂等的本质与实现层级幂等性(Idempotence)是分布式系统中的重要概念指的是对同一个操作执行一次或多次产生的效果是相同的。在充电订单场景中我们需要确保第一次请求创建新订单启动充电后续相同请求返回已存在的订单不执行实际充电操作实现幂等性通常有以下几种方式Token机制// 生成唯一token String token UUID.randomUUID().toString(); redisTemplate.opsForValue().set(order:token:userId, token, 5, TimeUnit.MINUTES); // 验证token String storedToken redisTemplate.opsForValue().get(order:token:userId); if(token.equals(storedToken)) { redisTemplate.delete(order:token:userId); // 处理业务 }唯一索引CREATE TABLE charging_orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, pile_id VARCHAR(32) NOT NULL, start_time DATETIME NOT NULL, UNIQUE KEY uk_user_pile (user_id, pile_id) );状态机校验public boolean startCharging(Long orderId) { ChargingOrder order orderRepository.findById(orderId); if(order.getStatus() ! OrderStatus.CREATED) { throw new IllegalStateException(订单已处理); } // 处理业务 }2.2 充电订单的幂等键设计对于充电订单场景我们需要精心设计幂等键(Idempotent Key)。一个好的幂等键应该包含用户标识user_id或account_id设备标识charging_pile_id或station_id时间窗口如按小时或天划分业务类型start_charging/stop_charging组合后的幂等键示例user:123:pile:ABC-001:action:start:20230515这种设计可以确保同一用户对同一充电桩的多次启动请求被视为重复不同用户或不同充电桩的请求不会冲突跨天的请求会被视为新请求3. 分布式锁的技术选型与实现3.1 Redis分布式锁的演进在分布式环境中简单的本地锁已无法满足需求。Redis因其高性能和丰富的数据结构成为实现分布式锁的首选方案。让我们看看Redis锁的演进过程第一代 - SETNX实现public boolean tryLock(String key, long expireTime) { long result redisTemplate.opsForValue() .setIfAbsent(key, 1, expireTime, TimeUnit.MILLISECONDS); return result 1; }问题锁过期时间难以预估可能提前释放第二代 - Redisson实现RLock lock redissonClient.getLock(order:lock:orderId); try { boolean acquired lock.tryLock(5, 30, TimeUnit.SECONDS); if(acquired) { // 处理业务 } } finally { lock.unlock(); }改进引入看门狗机制自动续期第三代 - RedLock算法Config config1 new Config(); config1.useSingleServer().setAddress(redis://127.0.0.1:6379); RedissonClient redisson1 Redisson.create(config1); // 创建多个Redisson实例 RLock lock1 redisson1.getLock(order:lock:orderId); RLock lock2 redisson2.getLock(order:lock:orderId); RLock lock3 redisson3.getLock(order:lock:orderId); // 联锁 RedissonRedLock lock new RedissonRedLock(lock1, lock2, lock3); try { boolean acquired lock.tryLock(); // 处理业务 } finally { lock.unlock(); }优势更高的可用性防止单点故障3.2 充电订单的锁粒度设计锁的粒度直接影响系统性能和并发能力。对于充电订单场景我们需要考虑用户级锁以user_id为维度优点防止同一用户重复下单缺点不同用户间无竞争设备级锁以charging_pile_id为维度优点防止同一设备被重复占用缺点同一用户多设备操作会串行化订单级锁以order_id为维度优点精确控制单个订单处理缺点无法防止重复创建订单推荐采用分层锁策略// 用户设备组合锁 String lockKey order:lock:user:userId:pile:pileId; RLock lock redissonClient.getLock(lockKey); try { boolean acquired lock.tryLock(3, 30, TimeUnit.SECONDS); if(acquired) { // 检查幂等 // 创建订单 } } finally { if(lock.isHeldByCurrentThread()) { lock.unlock(); } }4. 完整实现方案与代码解析4.1 系统架构设计基于慧知开源充电桩管理平台的防重复订单系统架构如下┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Client APP │───▶│ API Gateway │───▶│ Order Service │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ▲ ▲ │ │ │ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Charging Pile │◀────│ Pile Service │◀────│ Redis Cluster │ └─────────────────┘ └─────────────────┘ └─────────────────┘关键组件说明API Gateway实现请求防抖和初步校验Order Service核心业务逻辑实现幂等控制Redis Cluster存储分布式锁和幂等tokenPile Service充电桩状态管理4.2 核心代码实现订单服务层Service RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final RedissonClient redissonClient; private final OrderRepository orderRepository; private final RedisTemplateString, String redisTemplate; Override Transactional public OrderDTO createOrder(CreateOrderRequest request) { // 生成幂等键 String idempotentKey buildIdempotentKey(request); // 检查是否已处理 if (redisTemplate.opsForValue().setIfAbsent( order:idempotent: idempotentKey, 1, 24, TimeUnit.HOURS) Boolean.FALSE) { throw new BusinessException(重复请求); } // 获取分布式锁 String lockKey order:lock:user: request.getUserId() :pile: request.getPileId(); RLock lock redissonClient.getLock(lockKey); try { // 尝试获取锁等待3秒锁有效期30秒 boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(系统繁忙请稍后再试); } // 检查是否存在有效订单 OptionalChargingOrder existingOrder orderRepository .findByUserIdAndPileIdAndStatusIn( request.getUserId(), request.getPileId(), Arrays.asList(OrderStatus.CREATED, OrderStatus.CHARGING)); if (existingOrder.isPresent()) { return convertToDTO(existingOrder.get()); } // 创建新订单 ChargingOrder newOrder new ChargingOrder(); newOrder.setUserId(request.getUserId()); newOrder.setPileId(request.getPileId()); newOrder.setStatus(OrderStatus.CREATED); newOrder.setStartTime(LocalDateTime.now()); ChargingOrder savedOrder orderRepository.save(newOrder); return convertToDTO(savedOrder); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException(操作被中断); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } private String buildIdempotentKey(CreateOrderRequest request) { return String.format(user:%d:pile:%s:action:%s:date:%s, request.getUserId(), request.getPileId(), create, LocalDate.now().toString()); } }充电桩服务层Service RequiredArgsConstructor public class PileServiceImpl implements PileService { private final RedissonClient redissonClient; private final PileRepository pileRepository; Override public void startCharging(Long orderId, String pileId) { String lockKey pile:operation: pileId; RLock lock redissonClient.getLock(lockKey); try { boolean locked lock.tryLock(5, 60, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(设备忙请稍后再试); } ChargingPile pile pileRepository.findById(pileId) .orElseThrow(() - new ResourceNotFoundException(充电桩不存在)); if (pile.getStatus() ! PileStatus.IDLE) { throw new BusinessException(充电桩正在使用中); } pile.setStatus(PileStatus.CHARGING); pile.setCurrentOrderId(orderId); pileRepository.save(pile); // 实际调用硬件接口 // hardwareClient.startCharging(pileId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException(操作被中断); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }4.3 异常处理与补偿机制在分布式环境中网络分区和节点故障是常态我们需要完善的异常处理机制锁获取失败返回明确错误提示建议前端显示系统繁忙而非操作失败实现自动重试逻辑最多3次业务处理异常记录详细错误日志释放已获取的锁清除幂等标记允许重试锁释放失败实现锁超时自动释放添加监控告警提供手动释放接口补偿Job示例Scheduled(cron 0 */5 * * * ?) public void cleanStaleLocks() { SetString lockKeys redisTemplate.keys(order:lock:*); for (String key : lockKeys) { Long ttl redisTemplate.getExpire(key, TimeUnit.SECONDS); if (ttl ! null ttl -1) { // 没有设置过期时间 redisTemplate.expire(key, 30, TimeUnit.SECONDS); log.warn(Reset TTL for lock: {}, key); } } }5. 性能优化与生产实践5.1 压力测试与调优在实际部署前我们使用JMeter对系统进行了压力测试关键指标如下场景并发用户数平均响应时间错误率TPS无防护1000320ms12%450仅幂等控制1000410ms0.5%380幂等分布式锁1000580ms0%280优化后方案1000480ms0%350优化措施锁分段将用户ID哈希后分到16个锁段减少竞争本地缓存对最近成功的请求做短时间缓存异步记录将操作日志异步写入数据库连接池优化调整Redis连接池大小和超时参数优化后的锁获取逻辑// 根据用户ID哈希到不同的锁段 int lockSegment Math.abs(userId.hashCode()) % 16; String lockKey order:lock:segment: lockSegment :user: userId :pile: pileId;5.2 监控与告警配置完善的监控是生产环境稳定运行的保障我们建议配置以下监控项Redis监控内存使用率连接数慢查询键空间统计lock键数量业务监控订单创建成功率平均锁等待时间幂等拦截率订单状态异常率告警规则锁等待时间 1s 持续5分钟订单创建错误率 0.1%Redis内存使用 80%锁释放失败次数突增Prometheus配置示例- alert: HighLockWaitTime expr: avg(rate(order_lock_wait_seconds_sum[1m])) by (service) 1 for: 5m labels: severity: warning annotations: summary: High lock wait time in {{ $labels.service }} description: Average lock wait time is {{ $value }}s5.3 常见问题与解决方案问题1锁等待时间过长原因热点用户或设备导致锁竞争激烈解决方案增加锁分段数量实现公平锁队列优化业务流程减少临界区代码问题2Redis故障导致服务不可用原因Redis集群故障或网络分区解决方案实现多级降级本地锁 → 数据库乐观锁使用Redis哨兵或集群模式定期备份锁状态问题3幂等键冲突原因业务规则变化导致键碰撞解决方案在键中加入业务版本号实现键的自动过期定期清理历史键问题4锁泄漏原因应用崩溃未释放锁解决方案设置合理的锁超时实现锁的续期机制添加健康检查自动释放在实际生产环境中我们建议先在小规模充电站试点运行收集真实数据后再逐步推广到全平台。同时要保持与硬件团队的密切沟通因为充电桩的实际行为可能会影响软件层面的防重逻辑。