1. 充电订单防重复的核心挑战与解决方案
在充电桩管理平台的实际运营中,订单防重复是一个看似简单却暗藏玄机的技术点。我曾在多个充电桩项目现场遇到过因订单重复导致的计费纠纷,最严重的一次造成了单日近万元的财务损失。订单防重复机制的本质,是在高并发场景下保证交易唯一性的分布式系统问题。
充电桩业务场景的特殊性在于:
- 用户可能频繁点击开始充电按钮
- 网络延迟可能导致多次请求到达服务器
- 充电桩设备可能在离线状态下缓存操作指令
- 同一账户可能在多设备同时登录
这些场景都可能引发"同一充电会话被重复计费"的问题。我们采用的解决方案是"客户端幂等令牌+服务端分布式锁+数据库唯一索引"的三重防护机制。这个方案在慧知开源充电桩管理平台中经过两年实际检验,日均处理10万+订单零重复。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 防重复技术架构详解
2.1 客户端幂等令牌生成
前端在发起充电请求时,会生成全局唯一的requestId。这个ID的生成规则是:
java复制// 采用设备ID(4位) + 用户ID(6位) + 时间戳(10位) + 随机数(4位)
String requestId = String.format("%04d%06d%010d%04d",
deviceId,
userId,
System.currentTimeMillis()/1000,
ThreadLocalRandom.current().nextInt(10000));
这种组合方式可以确保:
- 同一设备上的不同用户不会冲突
- 同一用户在不同设备上不会冲突
- 同一用户在同一设备上的高频操作不会冲突
- 随机数作为最后的防碰撞保障
2.2 服务端分布式锁实现
服务端收到请求后,采用Redisson实现的分布式锁进行处理:
java复制RLock lock = redissonClient.getLock("charge:lock:" + connectorId);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 检查订单是否已存在
ChargeOrder existOrder = orderDao.findByConnectorAndStatus(
connectorId,
Arrays.asList(OrderStatus.STARTING, OrderStatus.CHARGING));
if (existOrder != null) {
throw new BusinessException("该充电桩已有进行中的订单");
}
// 创建新订单
return orderDao.create(newOrder);
}
} finally {
lock.unlock();
}
这里有几个关键点需要注意:
- 锁的粒度精确到具体充电枪(connectorId)
- 尝试获取锁的时间(3秒)要短于锁自动释放时间(10秒)
- 锁范围要覆盖订单查询和创建的全过程
- 必须放在finally块中释放锁
2.3 数据库唯一索引设计
在数据库层面,我们为orders表添加了组合唯一索引:
sql复制ALTER TABLE orders
ADD UNIQUE INDEX uk_connector_status (connector_id, status)
WHERE status IN ('STARTING', 'CHARGING');
这个设计实现了:
- 同一充电枪同时只能有一个进行中订单
- 数据库层最后的兜底防护
- 使用条件索引减少索引大小
3. 完整可运行代码实现
以下是慧知开源平台中订单服务的核心代码:
java复制@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderDao orderDao;
private final RedissonClient redissonClient;
private final RedisTemplate<String, String> redisTemplate;
@Transactional
@Override
public ChargeOrder startCharge(StartChargeRequest request) {
// 1. 检查幂等令牌
String idempotentKey = "charge:idempotent:" + request.getRequestId();
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(absent)) {
throw new BusinessException("重复的充电请求");
}
// 2. 获取分布式锁
RLock lock = redissonClient.getLock("charge:lock:" + request.getConnectorId());
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 3. 检查已有订单
ChargeOrder existOrder = orderDao.findByConnectorAndStatus(
request.getConnectorId(),
Arrays.asList(OrderStatus.STARTING, OrderStatus.CHARGING));
if (existOrder != null) {
if (existOrder.getUserId().equals(request.getUserId())) {
return existOrder; // 返回已有订单
}
throw new BusinessException("充电桩已被占用");
}
// 4. 创建新订单
ChargeOrder newOrder = new ChargeOrder();
// 订单数据填充...
return orderDao.create(newOrder);
}
throw new BusinessException("系统繁忙,请稍后再试");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("充电请求被中断");
} finally {
lock.unlock();
}
}
}
4. 实战中的经验与坑点
4.1 网络抖动导致的双订单问题
我们在灰度测试阶段遇到过这样的情况:客户端收到网络超时后重试,但实际上服务端已经处理成功。解决方案是在响应时返回服务器生成的唯一订单ID,客户端在重试时需要携带这个ID。
4.2 分布式锁的注意事项
- 锁的TTL设置要合理:太短会导致业务没执行完就释放,太长会影响系统可用性
- 避免锁重入问题:确保每次lock()都有对应的unlock()
- 锁的命名要有业务含义:方便问题排查和监控
4.3 数据库唯一索引的局限
唯一索引虽然能防止数据重复,但:
- 不能提供友好的错误提示
- 在高并发下会产生大量锁等待
- 需要配合业务逻辑使用
5. 性能优化方案
对于高并发场景,我们进一步优化了方案:
- 本地缓存加速:在应用层维护一个ConcurrentHashMap,缓存最近5分钟创建的订单,先查缓存再查DB
java复制private final ConcurrentMap<String, ChargeOrder> orderCache = new ConcurrentHashMap<>();
// 在create方法中添加
orderCache.put(order.getId(), order);
// 定时清理过期订单
- Redis预检机制:在获取分布式锁前,先用Redis的setnx检查
java复制Boolean canProceed = redisTemplate.opsForValue()
.setIfAbsent("charge:precheck:" + connectorId, "1", 1, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(canProceed)) {
throw new BusinessException("操作过于频繁");
}
- 异步日志记录:将订单创建日志通过消息队列异步处理,降低主流程延迟
6. 监控与告警设计
完善的监控体系能及时发现潜在问题:
- 重复订单率监控:
python复制# 监控脚本示例
duplicate_rate = (duplicate_orders.count() / total_orders.count()) * 100
if duplicate_rate > 0.1: # 超过0.1%即告警
alert("订单重复率异常升高")
- 锁竞争监控:
- 记录获取锁的平均等待时间
- 统计锁等待超时的次数
- 监控锁的持有时间分布
- 幂等令牌使用情况:
- 统计令牌重复使用的频率
- 监控令牌的过期时间设置是否合理
这套防重复方案在慧知开源充电桩管理平台的实际运行中,成功将重复订单率从最初的1.2%降到了0.001%以下。其中最关键的是理解业务场景,不能简单套用技术方案。比如在充电桩场景中,除了技术防重复,还需要考虑:
- 充电枪的物理状态同步
- 离线模式下的订单处理
- 用户主动停止又立即重试的特殊情况
开源项目地址中提供了完整的实现代码和测试用例,包括模拟高并发场景的压测脚本,可以帮助开发者快速理解和验证这套方案。在实际部署时,建议根据具体业务量调整锁的超时时间和重试策略。
