← 返回题目列表

策略模式如何支持配置化选择和灰度切换?

高频 困难 第 15 / 26 题 更新于 2026/08/02
策略模式配置化灰度发布动态切换

简化版

策略模式支持配置化和灰度切换时,要把策略实现、策略注册、选择规则和发布治理分开。策略类负责执行算法,注册表负责维护可用策略,选择器根据配置、用户、渠道、实验分组等上下文选策略。配置变更要做校验、灰度、回滚和监控,不能让线上请求直接依赖未校验的字符串。

详细版

在真实系统中,策略选择常常不是写死在代码里的,而是由配置中心、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。 并发请求可能读到半更新状态,应使用不可变快照或原子替换。
  • 追问:如何设计租户级策略选择? 先按租户规则匹配,再按灰度规则,最后走默认策略,并记录命中规则。
  • 追问:配置化会不会让代码难懂? 会,所以规则模型、命名、审计和可视化非常重要。
  • 追问:什么时候不适合配置化? 资金、权限、合规等强约束逻辑如果变更必须严格审批,就不要让普通配置随意切换。

九、加强记忆

  1. 策略实现回答“怎么做”,选择器回答“谁来做”。
  2. 配置规则要结构化,不能只靠字符串。
  3. 发布前校验 strategyKey、条件、优先级和默认策略。
  4. 灰度要稳定命中,常用用户哈希分桶。
  5. 动态刷新用不可变快照或原子替换。
  6. 配置化必须配监控、审计和回滚。