设计模式与大数据:MapReduce中的模板方法、Spark中的策略、Flink中的状态

做大数据开发这些年,我越来越觉得设计模式这东西,不是学出来的,是「长」出来的。你想想看,MapReduce、Spark、Flink 这三兄弟,本质上都在解决同一个问题——分布式计算。但它们的架构风格完全不同,背后的设计模式也各有侧重。

今天我就结合自己的实战经验,聊聊这三个框架里藏着的设计模式。嗯,说白了就是:MapReduce 的骨架、Spark 的策略、Flink 的状态

一、MapReduce 中的模板方法模式

先说说 MapReduce。我记得刚接触 Hadoop 时,最让我困惑的就是:为什么我只需要写 map 和 reduce 两个函数,其他事情框架都帮我干了?

这其实就是典型的模板方法模式。框架定义好了整个计算流程的骨架——输入分片、Map 阶段、Shuffle 排序、Reduce 阶段、输出结果。而开发者只需要填充两个「钩子方法」:map() 和 reduce()。

核心思想:父类定义算法骨架,子类实现具体步骤。流程固定,行为可变。

来看个实际例子。假设我们要做单词计数:

// Java 示例:MapReduce 中的模板方法
public class WordCount {

    // 这就是模板方法模式中的「钩子」
    public static class TokenizerMapper
        extends Mapper<Object, Text, Text, IntWritable> {

        private final static IntWritable one = new IntWritable(1);
        private Text word = new Text();

        // 开发者只需要实现这个方法
        public void map(Object key, Text value, Context context
                        ) throws IOException, InterruptedException {
            StringTokenizer itr = new StringTokenizer(value.toString());
            while (itr.hasMoreTokens()) {
                word.set(itr.nextToken());
                context.write(word, one);
            }
        }
    }

    // 另一个钩子
    public static class IntSumReducer
        extends Reducer<Text, IntWritable, Text, IntWritable> {

        private IntWritable result = new IntWritable();

        public void reduce(Text key, Iterable<IntWritable> values,
                           Context context
                           ) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }
            result.set(sum);
            context.write(key, result);
        }
    }
}

我在项目中遇到过一个问题:有个同事把大量初始化逻辑写在了 map() 方法里,结果每条记录都重复创建对象,性能惨不忍睹。后来我建议他把初始化放到 setup() 方法中——这也是模板方法提供的另一个钩子。

避坑指南:我曾经在 MapReduce 的 setup() 里加载了 200MB 的词典文件,结果每个 mapper 都加载一次,内存直接爆了。后来改成分布式缓存才解决。模板方法给了你钩子,但怎么用还得看场景。

二、Spark 中的策略模式

Spark 的设计思路完全不同。它不像 MapReduce 那样固定两阶段,而是提供了更灵活的算子组合。这里藏着的设计模式是策略模式

为什么会这样?因为 Spark 需要应对各种不同的计算场景——批处理、流处理、SQL 查询、图计算。如果每种场景都写一套固定流程,代码会变得极其臃肿。

策略模式的做法是:把算法封装成独立的策略类,运行时动态选择。在 Spark 中,每个 RDD 算子(map、filter、reduceByKey 等)本质上就是一个策略。

// Scala 示例:Spark 中的策略模式
// 不同的计算策略
trait ComputeStrategy {
  def execute(data: RDD[(String, Int)]): RDD[(String, Int)]
}

class SumStrategy extends ComputeStrategy {
  def execute(data: RDD[(String, Int)]): RDD[(String, Int)] = {
    data.reduceByKey(_ + _)
  }
}

class AverageStrategy extends ComputeStrategy {
  def execute(data: RDD[(String, Int)]): RDD[(String, Int)] = {
    data.groupByKey().mapValues(values => {
      val (sum, count) = values.aggregate((0, 0))(
        (acc, v) => (acc._1 + v, acc._2 + 1),
        (acc1, acc2) => (acc1._1 + acc2._1, acc1._2 + acc2._2)
      )
      sum / count
    })
  }
}

// 运行时动态选择策略
def processData(data: RDD[(String, Int)], strategy: ComputeStrategy): RDD[(String, Int)] = {
  strategy.execute(data)
}

我个人习惯把策略模式用在 Spark 的 ETL 流程中。比如数据清洗阶段,不同数据源需要不同的清洗逻辑,我就把它们封装成策略,通过配置文件动态加载。这样新增一种数据源时,完全不用改主流程代码。

注意:策略模式虽然灵活,但别滥用。我见过有人把每个 map 操作都封装成策略,结果策略类数量爆炸,维护成本反而更高。策略模式适合「算法族」的场景,而不是「每个小操作」。

三、Flink 中的状态模式

Flink 最核心的概念是什么?我个人觉得是「状态」。没有状态,流处理就是无状态的 map 操作,跟批处理没区别。而 Flink 的状态管理,背后是状态模式的思想。

状态模式的核心是:允许对象在内部状态改变时改变它的行为。在 Flink 中,每个算子都可以维护自己的状态,状态的变化驱动着计算逻辑的演进。

// Java 示例:Flink 中的状态管理
public class ThresholdAlertFunction
    extends KeyedProcessFunction<String, SensorReading, Alert> {

    // 使用 ValueState 维护状态
    private ValueState<Double> lastTemperature;

    @Override
    public void open(Configuration parameters) {
        // 状态描述符
        ValueStateDescriptor<Double> descriptor =
            new ValueStateDescriptor<>("lastTemp", Types.DOUBLE);
        lastTemperature = getRuntimeContext().getState(descriptor);
    }

    @Override
    public void processElement(SensorReading reading,
                               Context ctx,
                               Collector<Alert> out) throws Exception {

        Double prevTemp = lastTemperature.value();

        if (prevTemp != null) {
            double diff = Math.abs(reading.getTemperature() - prevTemp);
            if (diff > 10.0) {
                // 状态变化触发了告警行为
                out.collect(new Alert(
                    reading.getId(),
                    "温度突变: " + prevTemp + " -> " + reading.getTemperature()
                ));
            }
        }

        // 更新状态
        lastTemperature.update(reading.getTemperature());
    }
}

我记得有个项目是做实时风控,需要根据用户最近 5 分钟的行为来判断是否异常。如果用无状态计算,每次都要去查数据库,延迟根本扛不住。后来改成 Flink 的 ValueState + 定时器,状态保存在内存中,毫秒级响应。

关键点:Flink 的状态模式不仅仅是保存数据,更重要的是「状态驱动行为」。状态变了,计算逻辑也跟着变。这才是状态模式的精髓。

四、三种模式的对比与选择

说了这么多,我们来做个对比。我整理了一张表,方便你理解:

框架 设计模式 核心思想 适用场景 我的建议
MapReduce 模板方法 固定骨架,可变步骤 批处理,流程固定的任务 适合离线、定时任务
Spark 策略模式 算法封装,动态选择 多种计算模式混合 适合 ETL、交互式分析
Flink 状态模式 状态驱动行为变化 实时流处理、事件驱动 适合风控、监控、实时报表

你可能会问:这三种模式能混用吗?当然可以。我在一个项目中就用 Spark 做批处理(策略模式),同时用 Flink 做实时流处理(状态模式),中间用 HDFS 做数据交换。设计模式不是非此即彼,而是根据场景灵活组合。

五、一张图看懂整体脉络

下面这张 SVG 图展示了三种设计模式在大数据框架中的位置和关系。我画的时候特意把「骨架」「策略」「状态」这三个关键词突出出来,方便你记忆。

设计模式与大数据框架 MapReduce 模板方法模式 固定骨架:Map → Shuffle → Reduce 可变步骤:map() / reduce() 钩子 适合:离线批处理 Spark 策略模式 算法封装:map / filter / reduceByKey 动态选择:运行时切换策略 适合:ETL / 交互式分析 Flink 状态模式 状态驱动:ValueState / ListState 行为变化:状态改变 → 逻辑改变 适合:实时流处理 设计模式在大数据中的核心价值 🔹 模板方法:把不变的流程固定下来,把变化的逻辑交给开发者 🔹 策略模式:把算法封装成可替换的组件,灵活应对多变的需求 🔹 状态模式:让计算「记住」过去,从而做出更智能的决策 三种模式并非互斥,实际项目中经常组合使用

六、写在最后

说实话,设计模式和大数据框架的结合,不是让你去背概念,而是让你理解框架设计者的「良苦用心」。当你理解了 MapReduce 为什么用模板方法,你就知道什么时候该继承、什么时候该组合。当你理解了 Spark 的策略模式,你就知道如何设计可扩展的 ETL 流程。当你理解了 Flink 的状态模式,你就知道状态管理不是负担,而是流处理的核心能力。

我在带团队时经常说一句话:不要为了用模式而用模式,而是让模式自然地从问题中「长」出来。大数据框架已经帮我们做了最好的示范——它们不是先设计模式再写代码,而是在解决分布式计算的难题时,自然而然地用上了这些模式。

如果你正在学习设计模式,我建议你多看看这些开源框架的源码。不是让你全部读完,而是挑几个核心类,看看它们是怎么组织代码的。你会发现,设计模式其实就在你身边。

公众号:蓝海资料掘金营,微信 deep3321