支付渠道加权随机:用 Redis List 实现概率均匀的抽签池
·
背景
系统接入了十几个第三方支付渠道,每个渠道的质量参差不齐,根据实时成功率被评定为三个等级:
- 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 天然具有原子性,不需要额外加锁就能保证多实例消费的互不重叠。加上高峰时段动态扩容、等级不存在时的向上映射,整套方案在工程实践上兼顾了概率精确、分布式一致和动态调节三个目标。
更多推荐




所有评论(0)