支付渠道加权随机:用 Redis List 实现概率均匀的抽签池

📅 发布时间:2026/7/10 8:58:51 👁️ 浏览次数:
支付渠道加权随机:用 Redis List 实现概率均匀的抽签池
背景系统接入了十几个第三方支付渠道每个渠道的质量参差不齐根据实时成功率被评定为三个等级GGood优秀成功率高NNormal合格成功率正常UUnqualified不合格成功率低路由目标是按配置的权重比例把流量分配到不同等级的渠道同时做到在高并发下概率分布精确、不偏斜。最直觉的实现每次 random最简单的加权随机// 权重配置G6, N3, U1总权重 10 rand 随机数 [0, 总权重) 如果 rand G权重: 返回 G // 命中率 60% 如果 rand G权重N权重: 返回 N // 命中率 30% 否则: 返回 U // 命中率 10%这有什么问题理论上没问题但在高并发 分布式多实例的场景下每台机器各自 random短时间内受随机数波动影响实际概率可能短暂偏离比如 10 台机器同时处理一批请求可能连续多次都命中 GU 渠道长时间没有流量对于不合格渠道每 10 分钟限流 X 单这种配额控制纯随机会导致配额要么提前用完要么浪费问题的本质每次独立 random无法保证全局的概率分布是均匀的。改进方案预制概率池Pool 模式换一个思路与其每次抽签不如预先按比例把所有结果放进一个袋子随机打乱然后依次取出。权重配置G2, N2, U1 预制池打乱前[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 取到元素释放锁 进锁existstrue跳过 lpush ↓ ↓ (结束) lpop 取到元素或池子已被消耗完返回 null总结这套方案的核心思想是把概率提前物化成数量用预制数据结构代替运行时计算。在分布式场景下Redis List 的lpop天然具有原子性不需要额外加锁就能保证多实例消费的互不重叠。加上高峰时段动态扩容、等级不存在时的向上映射整套方案在工程实践上兼顾了概率精确、分布式一致和动态调节三个目标。