背景

系统接入了十几个第三方支付渠道,每个渠道的质量参差不齐,根据实时成功率被评定为三个等级:

  • G(Good):优秀,成功率高
  • N(Normal):合格,成功率正常
  • U(Unqualified):不合格,成功率低

路由目标是:按配置的权重比例,把流量分配到不同等级的渠道,同时做到在高并发下概率分布精确、不偏斜。

最直觉的实现:每次 random

最简单的加权随机:

// 权重配置:G=6, N=3, U=1,总权重 10
rand = 随机数 [0, 总权重)

如果 rand < G权重:     返回 "G"   // 命中率 60%
如果 rand < G权重+N权重: 返回 "N"  // 命中率 30%
否则:                  返回 "U"   // 命中率 10%

这有什么问题?

理论上没问题,但在高并发 + 分布式多实例的场景下:

  • 每台机器各自 random,短时间内受随机数波动影响,实际概率可能短暂偏离
  • 比如 10 台机器同时处理一批请求,可能连续多次都命中 G,U 渠道长时间没有流量
  • 对于"不合格渠道每 10 分钟限流 X 单"这种配额控制,纯随机会导致配额要么提前用完要么浪费

问题的本质:每次独立 random,无法保证全局的概率分布是均匀的

改进方案:预制概率池(Pool 模式)

换一个思路:与其每次抽签,不如预先按比例把所有结果放进一个袋子,随机打乱,然后依次取出

权重配置:G=2, N=2, U=1

预制池(打乱前):[G, G, N, N, U]
随机打乱后:     [N, G, U, G, N]

第1次取:N
第2次取:G
第3次取:U
第4次取:G
第5次取:N
袋子空了,重新填充并打乱...

这样就保证了:每一轮(5次)内,G 出现 2 次,N 出现 2 次,U 出现 1 次,概率完全精确

用 Redis List 实现分布式概率池

单机可以用内存 List,但分布式多实例场景需要共享状态,Redis List 是天然的选择:

  • lpush:批量填充池子
  • lpop:原子性地取出一个元素
  • exists:判断池子是否已初始化
用 Redis List 实现分布式概率池
单机可以用内存 List,但分布式多实例场景需要共享状态,Redis List 是天然的选择:
lpush:批量填充池子
lpop:原子性地取出一个元素
exists:判断池子是否已初始化

高峰时段流量倾斜

业务上有个特殊需求:高峰时段要让优质渠道承接更多流量

实现方式非常简单——高峰时段把整个池子扩大 3 倍

低峰时段(倍数=1):
    池子 = [G, G, N, N, U]          // 共 5 个槽

高峰时段(倍数=3):
    池子 = [G,G,G, G,G,G, N,N,N, N,N,N, U,U,U]  // 共 15 个槽

权重比例本身没有变,但池子更大,每轮覆盖的请求数更多,统计意义上概率分布更稳定。

高峰时段判断支持跨零点配置:

函数 是否高峰时段(国家配置 param):
    如果 高峰开始时间 或 高峰结束时间 未配置:
        返回 false

    当前小时 = 系统当前小时

    如果 结束时间 < 开始时间:          // 跨零点,例如 22:00-06:00
        返回 当前小时 >= 开始时间 或 当前小时 <= 结束时间
    否则:                             // 普通时段,例如 08:00-20:00
        返回 当前小时 >= 开始时间 且 当前小时 <= 结束时间

等级映射的边界处理

概率池返回的是"目标等级",但实际可用渠道不一定覆盖全部三个等级。比如当前只有 G 和 N 两个等级的渠道可用,池子却返回了 U,该怎么处理?

函数 按权重选等级(可用等级集合 levelSet, 国家配置 param):

    如果 levelSet 只有1个等级:
        返回 唯一的那个等级

    目标等级 = 从概率池取等级(param)

    如果 levelSet 包含全部3个等级:
        返回 目标等级              // 直接用,无需映射

    如果 levelSet 包含 目标等级:
        返回 目标等级              // 直接命中

    // 目标等级不在可用集合中,向上取近似值
    如果 目标等级 == "G": 返回 "N"  // 只有{N,U},G→N(取较优的)
    如果 目标等级 == "N": 返回 "G"  // 只有{G,U},N→G(优秀优先)
    如果 目标等级 == "U": 返回 "G"  // 只有{G,N},U→G(优秀优先)

    返回 levelSet 中随机一个        // 兜底

映射规则的设计原则:找不到目标等级时,尽量取更高质量的等级,保证用户体验。

方案对比总结

维度每次 random概率池(本文方案)
概率精确性统计意义上精确,短期有波动每轮内精确,波动极小
分布式一致性各实例独立,全局分布不保证Redis 共享池,全局一致
高峰调节修改权重参数重新计算扩大池子倍数,简单直接
实现复杂度极简中等(需考虑初始化竞争)
适用场景低并发、单机高并发、分布式多实例

一个值得注意的并发细节

初始化时存在一个极小概率的竞争窗口

线程1                          线程2
  ↓                              ↓
lpop 返回 null               lpop 返回 null
  ↓                              ↓
构建池子列表                  构建池子列表
  ↓                              ↓
抢到锁,lpush 成功            等待锁
  ↓                              ↓
lpop 取到元素,释放锁         进锁,exists=true,跳过 lpush
  ↓                              ↓
(结束)                        lpop 取到元素(或池子已被消耗完返回 null)

总结

这套方案的核心思想是:把"概率"提前物化成"数量",用预制数据结构代替运行时计算。

在分布式场景下,Redis List 的 lpop 天然具有原子性,不需要额外加锁就能保证多实例消费的互不重叠。加上高峰时段动态扩容、等级不存在时的向上映射,整套方案在工程实践上兼顾了概率精确分布式一致动态调节三个目标。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐