Java 案例:如何分配不同场景的权重
在业务系统中,"按场景分配权重"是非常常见的需求,推荐系统(不同召回渠道加权)、A/B 测试(流量按权重分流)、负载均衡(按机器性能分配流量)、风控评分(多规则加权)等。

下面从权重模型设计 → 核心算法 → 完整代码 → 工程化建议四个层面给出可落地的方案。
先明确权重的语义
在写代码前,必须先想清楚权重的语义,这决定了算法选型:
| 权重语义 | 说明 | 典型算法 |
|---|---|---|
| 比例权重 | 权重 3:1 表示流量按 75%/25% 分配 | 归一化 + 随机 / 累加区间 |
| 重要度权重 | 用于综合评分(加权平均) | 加权求和 / 加权平均 |
| 优先级权重 | 高权重先命中,失败了降级 | 排序 + 权重衰减 |
| 动态权重 | 随时间/状态变化 | 滑动窗口 + 自适应 |
常见的陷阱:权重总量不固定,例如新增一个场景后,总和从 100 变成 130,如果直接按
random(100)取,会导致部分场景永远命中不到。必须先归一化。
核心算法:加权随机(最常用)
方案 1:区间累加法(O(n) 查找,简单直观)
public class WeightedRandomSelector<T> {
private final List<T> items = new ArrayList<>();
private final List<Double> cumulativeWeights = new ArrayList<>();
private double totalWeight = 0;
/** 添加场景与权重 */
public void add(T item, double weight) {
if (weight <= 0) throw new IllegalArgumentException("weight must > 0");
totalWeight += weight;
items.add(item);
cumulativeWeights.add(totalWeight);
}
/** 加权随机选择 */
public T select() {
if (items.isEmpty()) throw new IllegalStateException("no items");
// [0, totalWeight)
double r = ThreadLocalRandom.current().nextDouble() * totalWeight;
// 二分查找第一个 cumulative >= r 的位置
int idx = Collections.binarySearch(cumulativeWeights, r);
if (idx < 0) idx = -idx - 1;
return items.get(idx);
}
}
使用示例:
WeightedRandomSelector<String> selector = new WeightedRandomSelector<>();
selector.add("首页推荐", 50);
selector.add("搜索兜底", 30);
selector.add("猜你喜欢", 15);
selector.add("运营位", 5);
// 调用 10000 次验证分布
Map<String, Integer> counter = new HashMap<>();
for (int i = 0; i < 10000; i++) {
counter.merge(selector.select(), 1, Integer::sum);
}
System.out.println(counter); // 大致 5000/3000/1500/500
优点:简单、内存小、二分查找 O(log n)
缺点:新增/删除时需重建数组(高频变更场景不友好)
方案 2:Alias Method(O(1) 采样,适合高频调用 + 静态权重)
当 QPS 很高(如推荐在线服务)时,可用 Alias Method 把单次采样降到 O(1)。
public class AliasMethodSelector<T> {
private final List<T> items;
private final double[] prob;
private final int[] alias;
public AliasMethodSelector(List<T> items, List<Double> weights) {
int n = items.size();
this.items = items;
this.prob = new double[n];
this.alias = new int[n];
double sum = weights.stream().mapToDouble(Double::doubleValue).sum();
double[] p = new double[n];
double avg = sum / n;
Deque<Integer> small = new ArrayDeque<>(), large = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
p[i] = weights.get(i) / avg;
if (p[i] < 1.0) small.push(i); else large.push(i);
}
while (!small.isEmpty() && !large.isEmpty()) {
int s = small.pop(), l = large.pop();
prob[s] = p[s];
alias[s] = l;
p[l] = (p[l] + p[s]) - 1.0;
if (p[l] < 1.0) small.push(l); else large.push(l);
}
while (!large.isEmpty()) prob[large.pop()] = 1.0;
while (!small.isEmpty()) prob[small.pop()] = 1.0;
}
public T select() {
int i = ThreadLocalRandom.current().nextInt(items.size());
return ThreadLocalRandom.current().nextDouble() < prob[i] ? items.get(i) : items.get(alias[i]);
}
}
适用:权重固定、QPS 极高(广告/推荐召回层)
完整场景案例:多场景实验分流
真实业务中常需要"按场景给实验分组分配权重",例如灰度发布。
public enum Scene { HOME, SEARCH, DETAIL, CART, ORDER }
public record ExperimentGroup(String name, Map<Scene, Double> sceneWeights) {}
public class SceneWeightedExperiment {
// 每个场景独立的权重选择器
private final Map<Scene, WeightedRandomSelector<ExperimentGroup>> selectors = new EnumMap<>(Scene.class);
public SceneWeightedExperiment(List<ExperimentGroup> groups) {
for (Scene scene : Scene.values()) {
WeightedRandomSelector<ExperimentGroup> sel = new WeightedRandomSelector<>();
for (ExperimentGroup g : groups) {
Double w = g.sceneWeights().get(scene);
if (w != null && w > 0) sel.add(g, w);
}
if (!sel.isEmpty()) selectors.put(scene, sel);
}
}
public String assign(Scene scene) {
WeightedRandomSelector<ExperimentGroup> sel = selectors.get(scene);
return sel == null ? "default" : sel.select().name();
}
}
使用:
List<ExperimentGroup> groups = List.of(
new ExperimentGroup("A", Map.of(Scene.HOME, 50.0, Scene.SEARCH, 20.0)),
new ExperimentGroup("B", Map.of(Scene.HOME, 30.0, Scene.SEARCH, 60.0)),
new ExperimentGroup("C", Map.of(Scene.HOME, 20.0, Scene.SEARCH, 20.0))
);
SceneWeightedExperiment exp = new SceneWeightedExperiment(groups);
System.out.println(exp.assign(Scene.HOME)); // 按 50/30/20 分布
System.out.println(exp.assign(Scene.SEARCH)); // 按 20/60/20 分布
多维度综合评分(加权求和场景)
当需求不是"分流"而是"打分",用加权平均即可:
public class WeightedScorer {
// 维度及其权重
private final Map<String, Double> weights;
public WeightedScorer(Map<String, Double> weights) {
double sum = weights.values().stream().mapToDouble(Double::doubleValue).sum();
if (sum <= 0) throw new IllegalArgumentException("权重总和必须 > 0");
// 归一化
this.weights = weights.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue() / sum));
}
public double score(Map<String, Double> features) {
double total = 0;
for (var e : weights.entrySet()) {
double v = features.getOrDefault(e.getKey(), 0.0);
total += v * e.getValue();
}
return total;
}
}
示例:风控评分
WeightedScorer scorer = new WeightedScorer(Map.of(
"设备风险", 0.4,
"行为异常", 0.35,
"历史黑名单", 0.25
));
double risk = scorer.score(Map.of(
"设备风险", 0.8,
"行为异常", 0.6,
"历史黑名单", 0.1
));
System.out.println(risk); // 0.8*0.4 + 0.6*0.35 + 0.1*0.25 = 0.555
工程化建议(踩坑清单)
- 权重归一化:不要假设权重总和为 100,代码里统一
/ sum。 - 权重配置化:放到配置中心(Nacos/Apollo),支持热更新,避免重启。
- 权重合法性校验:负数、全零、NaN 都要拦截。
- 可观测性:埋点记录每次命中,定期比较实际分布 vs 期望分布(卡方检验)。
- 确定性分流:如果同一用户要保持稳定分组,用
hash(userId + scene) % totalWeight而不是纯随机。 - 降级策略:某个场景返回空时要有 default 兜底。
- 性能:QPS 高且权重稳定 → Alias Method;权重频繁变 → 区间累加之。
- 整数 vs 浮点:如果可能,用整数权重(乘 1000 转整数)避免浮点误差。
确定性分流(同一用户稳定命中)
public String stableAssign(String userId, Scene scene, List<ExperimentGroup> groups) {
long total = groups.stream().mapToLong(g -> (long)(g.sceneWeights().getOrDefault(scene, 0.0) * 1000)).sum();
if (total == 0) return "default";
long hash = Math.abs((userId + ":" + scene).hashCode()) % total;
long cum = 0;
for (ExperimentGroup g : groups) {
cum += (long)(g.sceneWeights().getOrDefault(scene, 0.0) * 1000);
if (hash < cum) return g.name();
}
return groups.get(groups.size() - 1).name();
}
用
hash(userId + scene)而不是hash(userId),可以让同一用户在不同场景命中不同分组(场景间正交),这也是互斥实验的标准做法。
总结选型
| 场景 | 推荐方案 |
|---|---|
| 学习/低 QPS/权重常变 | 区间累加 + 二分 |
| 高 QPS + 静态权重 | Alias Method |
| 多场景独立权重 | 每场景一个 selector |
| 综合评分 | 归一化 + 加权求和 |
| 需要用户稳定分组 | 哈希 + 累加区间 |
| 权重需动态调整 | 配置中心热更新 + 重建 |
如果你能告诉我具体是哪类业务场景(流量分流 / 打分 / 排序 / 推荐召回),我可以给一版更贴合生产环境的完整实现。