策略模式如何支持配置化选择和灰度切换?
简化版
策略模式支持配置化和灰度切换时,要把策略实现、策略注册、选择规则和发布治理分开。策略类负责执行算法,注册表负责维护可用策略,选择器根据配置、用户、渠道、实验分组等上下文选策略。配置变更要做校验、灰度、回滚和监控,不能让线上请求直接依赖未校验的字符串。
详细版
在真实系统中,策略选择常常不是写死在代码里的,而是由配置中心、AB 实验、租户配置或灰度规则决定。例如:
tenant=A -> price-v2
tenant=B -> price-v1
userId hash 0-9 -> price-v3-gray
fallback -> price-v1
这时策略模式要加一层“选择器”:
class StrategySelector {
Strategy select(RequestContext context) {
String key = ruleConfig.match(context);
return registry.get(key);
}
}
重点是配置 key 必须能映射到真实策略,规则变更必须可回滚,灰度命中必须可观测。否则配置化会从灵活性变成线上风险源。
记忆钩子:策略实现解决“怎么做”,配置化选择解决“此刻让谁做”,灰度治理解决“出问题怎么退”。
完整版教学
面试提示:配置化策略切换要讲清楚灰度、回滚、默认策略和可观测性,面试官很爱追问线上事故。
一、为什么策略选择要配置化
如果每次新增或切换策略都要发版,业务响应会很慢。常见需求包括:
- 不同租户使用不同计费策略;
- 新算法只给 5% 用户灰度;
- 某个渠道临时切回旧策略;
- 大促期间启用专用优惠策略;
- AB 实验比较两个推荐策略效果。
这些场景都要求“策略实现由代码提供,策略选择由配置控制”。
二、把策略实现和选择规则拆开
策略实现只关心业务计算:
interface PriceStrategy {
String key();
Money calculate(PriceContext context);
}
注册表负责 key 到策略对象:
class StrategyRegistry {
private final Map<String, PriceStrategy> strategies;
PriceStrategy get(String key) {
PriceStrategy strategy = strategies.get(key);
if (strategy == null) {
throw new IllegalArgumentException("unknown strategy: " + key);
}
return strategy;
}
}
选择器负责根据配置匹配:
class PriceStrategySelector {
PriceStrategy select(PriceContext context) {
String key = config.match(context.tenantId(), context.userId(), context.channel());
return registry.get(key);
}
}
这样实现、注册和选择各自独立。
三、配置规则要有明确模型
不要让配置变成随意字符串。可以设计结构化规则:
priceStrategyRules:
- name: tenant-a-v2
condition:
tenantId: A
strategyKey: price-v2
priority: 100
- name: gray-v3
condition:
userHashRange: 0-9
strategyKey: price-v3
priority: 80
- name: default
strategyKey: price-v1
priority: 0
每条规则至少包含名称、条件、目标策略、优先级和启停状态。配置越结构化,越容易校验、审计和回滚。
四、配置发布前必须校验
配置化策略最怕“配置写错,线上才发现”。校验点包括:
| 校验项 | 例子 |
|---|---|
| strategyKey 是否存在 | price-v9 没有对应 Bean |
| priority 是否冲突 | 两条规则同条件同优先级 |
| 条件是否合法 | hash 范围不能超过 0-99 |
| 默认策略是否存在 | 没有 default 规则 |
| 策略是否允许灰度 | 扣款策略可能禁止实验 |
配置中心最好在发布前执行这些校验,应用启动时也要做一次防线。
五、灰度切换要稳定命中
灰度不能每次请求随机,否则同一个用户可能一会儿 v1、一会儿 v2。更常见的是按用户哈希:
int bucket = Math.abs(userId.hashCode()) % 100;
if (bucket < 10) {
return "price-v3";
}
return "price-v1";
这样同一个用户稳定落在同一桶里,便于观察指标和复现问题。
六、动态刷新要考虑一致性
配置动态刷新时,不要边读边改可变 Map。可以使用不可变快照:
class RuleConfigHolder {
private final AtomicReference<List<Rule>> rules = new AtomicReference<>(List.of());
void refresh(List<Rule> newRules) {
validate(newRules);
rules.set(List.copyOf(newRules));
}
List<Rule> currentRules() {
return rules.get();
}
}
每个请求读取到的是某个完整版本的规则,不会看到更新到一半的状态。
七、灰度必须配合监控和回滚
策略切换后要观察:
strategy_key=price-v3
hit_count=12000
error_rate=0.02%
avg_price_delta=-3.8
fallback_count=17
如果新策略错误率升高或关键业务指标异常,要能快速切回旧策略。灰度的价值不只是小流量试验,而是让问题影响范围可控。
八、常见误区与追问
- 误区:策略 key 写在配置里就算配置化。 还需要结构化规则、校验、灰度、回滚和监控。
- 误区:配置错了走默认策略就行。 高风险策略 key 错误应该阻断发布或报警,不能悄悄默认。
- 误区:灰度可以每次随机。 随机会导致用户体验不稳定,也不利于问题复现。
- 误区:动态刷新就是改全局 Map。 并发请求可能读到半更新状态,应使用不可变快照或原子替换。
- 追问:如何设计租户级策略选择? 先按租户规则匹配,再按灰度规则,最后走默认策略,并记录命中规则。
- 追问:配置化会不会让代码难懂? 会,所以规则模型、命名、审计和可视化非常重要。
- 追问:什么时候不适合配置化? 资金、权限、合规等强约束逻辑如果变更必须严格审批,就不要让普通配置随意切换。
九、加强记忆
- 策略实现回答“怎么做”,选择器回答“谁来做”。
- 配置规则要结构化,不能只靠字符串。
- 发布前校验 strategyKey、条件、优先级和默认策略。
- 灰度要稳定命中,常用用户哈希分桶。
- 动态刷新用不可变快照或原子替换。
- 配置化必须配监控、审计和回滚。