Random总打同一台机器因多线程共用默认种子导致序列重复;应避免方法内new Random()或静态实例,改用Random.Shared;轮询需用Interlocked.Increment或ConcurrentQueue保证线程安全;列表变更时用ImmutableArray或重建实例确保热更新安全。

用 Random 做随机负载均衡,为什么总打到同一台机器?
因为没管好 Random 实例的生命周期——在高并发下,多个线程用默认构造的 Random(基于系统时钟种子),极易生成重复序列。
- 别在方法里每次
new Random(),尤其别在循环或高频调用中 - 用静态只读实例:
private static readonly Random _random = new Random(); - 如果真需要线程安全,改用
Random.Shared(.NET 6+):直接调Random.Shared.Next(list.Count) - 注意:即使随机,也不等于“均匀”。小样本(比如只有2台服务)下肉眼可见扎堆,这是概率现象,不是 bug
IEnumerable<string> 列表轮询(Round Robin),怎么避免状态错乱?
轮询必须维护当前索引,但多线程共享一个计数器会丢偏移、重复或跳过。不能简单用 static int _index。
- 用
Interlocked.Increment原子递增:int i = Interlocked.Increment(ref _counter) % servers.Count; - 更稳妥是用
ConcurrentQueue<string>配合重入队列(把取出来的放回末尾),但有额外分配开销 - 如果服务列表会动态变更,轮询状态和列表长度不同步会导致
IndexOutOfRangeException—— 每次取前先缓存var count = servers.Count,再算模 - .NET 5+ 可考虑
System.Collections.Concurrent.ConcurrentBag<string>,但它是无序的,不适用轮询场景
服务地址列表变了,负载均衡器怎么热更新不中断?
硬编码或只读字段存地址列表,改配置就得重启;用变量存又怕并发读写冲突。
- 用
Volatile.Read+Volatile.Write控制可见性(适用于简单替换整个列表) - 推荐
ImmutableArray<string>:更新时_servers = ImmutableArray.CreateRange(newList);,读取无锁且线程安全 - 别直接暴露
List<string>给外部修改——哪怕加了锁,调用方仍可能拿到引用后私自改内容 - 如果走配置中心(如 Consul、Nacos),监听变更后重建均衡器实例比原地更新更安全,避免状态残留
要不要自己写负载均衡?HttpClient 和 HttpMessageHandler 已经做了什么?
大多数情况下,你不需要从零实现——HttpClient 本身不负责服务发现和路由,但它配合 SocketsHttpHandler 的连接池,已经隐式做了连接粒度的负载(复用 TCP 连接)。真正要插手的是「选哪个地址发请求」这一步。
- 如果你用
HttpClient+ 手动拼http://host:port/api,那负载逻辑就在你手里,上面几种策略都适用 - 如果用
Microsoft.Extensions.Http的命名客户端 +Polly重试,建议把负载逻辑封装进自定义DelegatingHandler,统一拦截HttpRequestMessage.RequestUri并改写 host - 注意:DNS 轮询(如 nginx upstream)和客户端轮询不是一回事。后者可控、实时,前者受 TTL 和本地 DNS 缓存影响,可能几分钟都不生效
真实环境里,服务节点上下线、网络抖动、慢节点识别……这些一加上,简单的随机或轮询很快就不够用。但起步阶段,只要管住 Random 生命周期和列表更新的线程安全,就已经避开了 80% 的线上故障。










