基于权重与历史记录的Spring Boot短信供应商动态选择实现问询
基于历史发送记录的短信供应商权重平衡实现方案
核心思路
放弃固定窗口的权重分配逻辑,转而基于全量发送历史动态调整供应商选中概率,让累计发送占比持续向预设权重靠拢。无需预先知晓每日发送量,系统会根据实时的发送偏差自动调整优先级:落后于期望占比的供应商获得更高选中概率,超额的则暂时降低优先级,最终长期累计占比无限接近设定权重。
具体实现步骤
1. 存储发送统计数据
需要记录两个维度的发送量,推荐用Redis存储(十万级读写性能完全够用):
- 累计总发送量:所有时间窗口的发送总和,用于计算全局期望占比,键值结构:
sms_vendor_total:{vendorId}(数值类型) - 当前窗口发送量:仅统计当日发送量,用于监控窗口内表现(可选),键值结构:
sms_vendor_window:{vendorId}(数值类型)
2. 动态权重计算逻辑
每次选择供应商时执行以下步骤:
- 过滤可用供应商:排除服务不可用的供应商,仅在可用范围内计算
- 计算全局累计总发送量:统计所有可用供应商的累计发送量之和
- 计算每个供应商的偏差值:
偏差值为正说明该供应商未达期望占比,需要优先分配;为负则说明已超额,暂时降低优先级。期望累计发送量 = 全局累计总发送量 × 供应商权重百分比(如20%则乘0.2) 偏差值 = 期望累计发送量 - 实际累计发送量 - 生成动态权重:将偏差值取非负值,同时给每个供应商设置最小权重1(防止偏差值为0时永远不被选中),以此作为当前选择的权重依据
- 随机选择供应商:基于动态权重的总和生成随机数,按权重区间匹配选中的供应商
3. 窗口重置与统计更新
- 每次发送成功后,同步更新该供应商的累计发送量和当前窗口发送量
- 每日凌晨通过定时任务重置当前窗口的发送量统计(累计发送量保留)
4. 特殊场景处理
- 模板指定供应商:直接使用指定供应商,同时更新统计数据
- 供应商不可用:实时过滤不可用供应商,仅在可用列表中执行平衡逻辑
代码改造示例
核心选择逻辑
if (Objects.nonNull(templateEntity.getSmsVendor())) { // 模板指定供应商,直接发送并更新统计 SmsNotifier specifiedNotifier = smsNotifier.get(templateEntity.getSmsVendor()); specifiedNotifier.send(notificationRequestDTO); updateVendorSendStats(specifiedNotifier.getId()); return; } // 过滤可用供应商 List<SmsNotifier> availableVendors = smsNotifierList.stream() .filter(SmsNotifier::isAvailable) .collect(Collectors.toList()); if (availableVendors.isEmpty()) { throw new IllegalStateException("无可用短信供应商"); } // 计算全局累计总发送量 long totalSent = availableVendors.stream() .mapToLong(vendor -> getVendorTotalSent(vendor.getId())) .sum(); // 计算每个供应商的动态权重(偏差值) List<Map.Entry<SmsNotifier, Long>> dynamicWeightList = new ArrayList<>(); for (SmsNotifier vendor : availableVendors) { double expectedTotal = totalSent * (vendor.getWeightage() / 100.0); long actualTotal = getVendorTotalSent(vendor.getId()); // 偏差值取非负,且最小权重设为1,避免无法被选中 long dynamicWeight = Math.max((long) Math.ceil(expectedTotal - actualTotal), 1); dynamicWeightList.add(new AbstractMap.SimpleEntry<>(vendor, dynamicWeight)); } // 计算动态权重总和 long totalDynamicWeight = dynamicWeightList.stream() .mapToLong(Map.Entry::getValue) .sum(); // 随机选择供应商 long randomNum = RandomUtils.nextLong(0, totalDynamicWeight); long currentWeight = 0; for (Map.Entry<SmsNotifier, Long> entry : dynamicWeightList) { SmsNotifier selectedVendor = entry.getKey(); long weight = entry.getValue(); if (randomNum >= currentWeight && randomNum < currentWeight + weight) { selectedVendor.send(notificationRequestDTO); updateVendorSendStats(selectedVendor.getId()); break; } currentWeight += weight; }
统计操作与定时任务
@Autowired private StringRedisTemplate redisTemplate; // 更新供应商发送统计 private void updateVendorSendStats(String vendorId) { // 累计发送量+1 redisTemplate.opsForValue().increment("sms_vendor_total:" + vendorId); // 当前窗口发送量+1 redisTemplate.opsForValue().increment("sms_vendor_window:" + vendorId); } // 获取供应商累计发送量 private long getVendorTotalSent(String vendorId) { String totalStr = redisTemplate.opsForValue().get("sms_vendor_total:" + vendorId); return totalStr != null ? Long.parseLong(totalStr) : 0; } // 每日凌晨重置当前窗口统计 @Scheduled(cron = "0 0 0 * * ?") public void resetDailyWindowStats() { for (SmsNotifier vendor : smsNotifierList) { redisTemplate.delete("sms_vendor_window:" + vendor.getId()); } }
关键细节说明
- 全量历史的优势:无需预测每日发送量,无论单日发送10条还是10万条,系统都会自动调整选中概率,长期累计占比会稳定趋近于预设权重
- 最小权重1的必要性:避免供应商因偏差值为0而完全无法被选中,保证即使达标后仍有机会分配到请求,维持权重的动态平衡
- 性能保障:Redis读写为O(1),动态权重计算仅遍历供应商列表(数量通常很少),十万级发送量下完全能支撑实时处理
- 容错性:供应商不可用会被实时过滤,恢复后自动纳入平衡逻辑,不会影响整体发送稳定性
内容的提问来源于stack exchange,提问作者Anurator
相关产品推荐
相关产品推荐

