轮询必须用原子操作管理索引,权重变更需重新归一化并更新累积前缀和,节点增删应原子替换快照;加权轮询须用整数随机值+二分查找累积数组,禁用线性遍历与浮点运算。

轮询逻辑不能只靠计数器,得用原子操作防并发错乱
直接用 int 变量做轮询索引在多 goroutine 场景下必然出错——比如两个请求同时读到索引 2,都去选第 2 个节点,下一个请求又从 3 开始,跳过第 0 个。必须用 atomic.Int64 或 atomic.Load/StoreUint64 管理当前位置。更稳妥的做法是把“取下一个”和“更新位置”合并为一个原子操作:atomic.AddUint64(&pos, 1),再对节点总数取模。
注意:取模运算本身不是原子的,所以得先原子递增,再本地计算索引,避免中间态被其他 goroutine 干扰。
权重动态修改必须触发“重新归一化”,否则新旧权重混算
如果只是允许调用方改某个节点的 weight 字段,而不重置内部状态,轮询结果会严重偏离预期。例如原有权重 [3, 1, 1],总和为 5;把第一个节点权重改为 1 后,若不重新计算,后续仍按旧比例分配,导致该节点流量远高于 1/(1+1+1) = 33%。
- 每次调用
SetWeight(nodeID, newWeight)时,必须同步更新全局权重数组,并重新计算总权重和累积权重前缀和(用于加权轮询) - 推荐用
sync.RWMutex保护权重数据结构,读多写少场景下比全互斥更高效 - 避免在每次路由选择时实时计算前缀和——它只在权重变更时变,应缓存为
[]uint64数组
加权轮询不能只靠随机数,要用累积权重二分查找
常见误区是生成一个 rand.Float64() * totalWeight,然后线性遍历找区间。这在节点数多(>100)时性能差,且无法保证严格按权重比例收敛。正确做法是预计算累积权重数组(如权重 [2,3,1] → 前缀和 [2,5,6]),再用 sort.Search 在该数组中二分查找首个 ≥ 随机值的位置。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
示例关键片段:
// weights 是已归一化的 uint64 累积数组,如 [2,5,6]
idx := sort.Search(len(weights), func(i int) bool {
return weights[i] >= randVal
})
注意:randVal 必须是 [1, totalWeight] 范围内的整数(非浮点),否则边界可能越界或漏项;用 rand.Int63n(int64(total)) + 1 更安全。
节点增删需重建权重快照,避免索引错位或 panic
如果允许运行时增删节点,但只修改原始 slice,会导致两个问题:一是已有 goroutine 正在遍历旧 slice,二是累积权重数组长度与节点数不一致,sort.Search 可能 panic 或返回越界索引。
- 每次
AddNode()或RemoveNode()都应生成全新权重快照(新 slice + 新前缀和数组),再用原子指针替换旧快照(atomic.StorePointer) - 读取时用
atomic.LoadPointer获取当前快照,确保看到的是完整、一致的状态 - 不要在路由热路径里做任何 slice append 或 map 查找——这些操作无法保证原子性
真正难的不是轮询算法本身,而是让权重变更、节点变更、高并发路由三者互不干扰。多数线上事故都发生在「以为改个字段就生效」,结果旧快照还在服务,新权重却已写入,中间态持续几十毫秒甚至更久。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










