You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于权重与历史记录的Spring Boot短信供应商动态选择实现问询

基于历史发送记录的短信供应商权重平衡实现方案

核心思路

放弃固定窗口的权重分配逻辑,转而基于全量发送历史动态调整供应商选中概率,让累计发送占比持续向预设权重靠拢。无需预先知晓每日发送量,系统会根据实时的发送偏差自动调整优先级:落后于期望占比的供应商获得更高选中概率,超额的则暂时降低优先级,最终长期累计占比无限接近设定权重。

具体实现步骤

1. 存储发送统计数据

需要记录两个维度的发送量,推荐用Redis存储(十万级读写性能完全够用):

  • 累计总发送量:所有时间窗口的发送总和,用于计算全局期望占比,键值结构:sms_vendor_total:{vendorId}(数值类型)
  • 当前窗口发送量:仅统计当日发送量,用于监控窗口内表现(可选),键值结构:sms_vendor_window:{vendorId}(数值类型)

2. 动态权重计算逻辑

每次选择供应商时执行以下步骤:

  1. 过滤可用供应商:排除服务不可用的供应商,仅在可用范围内计算
  2. 计算全局累计总发送量:统计所有可用供应商的累计发送量之和
  3. 计算每个供应商的偏差值:
    期望累计发送量 = 全局累计总发送量 × 供应商权重百分比(如20%则乘0.2)
    偏差值 = 期望累计发送量 - 实际累计发送量
    
    偏差值为正说明该供应商未达期望占比,需要优先分配;为负则说明已超额,暂时降低优先级。
  4. 生成动态权重:将偏差值取非负值,同时给每个供应商设置最小权重1(防止偏差值为0时永远不被选中),以此作为当前选择的权重依据
  5. 随机选择供应商:基于动态权重的总和生成随机数,按权重区间匹配选中的供应商

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 14:54:53