Apache Spark Java 示例:Word Count(词频统计)

Apache Spark Java 示例:Word Count(词频统计)

Apache Spark 是当前最流行的大数据处理框架之一,Word Count 程序作为其最经典的入门示例,展示了分布式计算的核心概念。以下是一个完整的 Spark Java 实现的词频统计示例。

完整实现代码

import org.apache.spark.api.java.JavaPairRDD;
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.api.java.JavaSparkContext;
import org.apache.spark.sql.SparkSession;
import scala.Tuple2;

import java.util.Arrays;
import java.util.List;

public class SparkWordCount {

    public static void main(String[] args) {
        // 检查参数
        if (args.length < 2) {
            System.err.println("Usage: SparkWordCount <inputPath> <outputPath>");
            System.exit(1);
        }

        String inputPath = args[0];
        String outputPath = args[1];

        // 1. 创建SparkSession(Spark 2.0+)
        SparkSession spark = SparkSession.builder()
                .appName("Java Word Count")
                //.master("local[*]") // 本地模式调试时使用
                .getOrCreate();

        // 2. 从SparkSession创建JavaSparkContext
        JavaSparkContext jsc = new JavaSparkContext(spark.sparkContext());

        try {
            // 3. 从HDFS或本地文件系统读取文本文件
            JavaRDD<String> lines = jsc.textFile(inputPath);

            // 4. 将每行拆分成单词(扁平化操作)
            JavaRDD<String> words = lines.flatMap(line -> 
                Arrays.asList(line.split("\\s+")).iterator()
            );

            // 5. 过滤无效字符并转换为小写
            JavaRDD<String> cleanedWords = words.map(word -> 
                word.replaceAll("[^a-zA-Z]", "").toLowerCase()
            ).filter(word -> !word.isEmpty());

            // 6. 映射为 (word, 1) 的键值对
            JavaPairRDD<String, Integer> wordPairs = cleanedWords.mapToPair(word -> 
                new Tuple2<>(word, 1)
            );

            // 7. 使用reduceByKey对相同单词的计数进行汇总
            JavaPairRDD<String, Integer> wordCounts = wordPairs.reduceByKey((a, b) -> a + b);

            // 8. 按词频排序(降序)
            JavaPairRDD<Integer, String> countWords = wordCounts.mapToPair(tuple -> 
                new Tuple2<>(tuple._2(), tuple._1())
            );
            
            JavaPairRDD<Integer, String> sortedCounts = countWords.sortByKey(false);
            
            // 9. 结果转换回 (word, count) 格式
            JavaPairRDD<String, Integer> sortedWordCounts = sortedCounts.mapToPair(tuple -> 
                new Tuple2<>(tuple._2(), tuple._1())
            );

            // 10. 结果输出到文件系统
            sortedWordCounts.saveAsTextFile(outputPath);

            // 11. 收集前10个结果用于控制台显示
            List<Tuple2<String, Integer>> topWords = sortedWordCounts.take(10);

            // 12. 在控制台打印结果
            System.out.println("\n==================== 词频统计Top10 ====================");
            for (Tuple2<String, Integer> tuple : topWords) {
                System.out.printf("%-15s: %d%n", tuple._1(), tuple._2());
            }
            System.out.println("=====================================================");

        } catch (Exception e) {
            System.err.println("执行过程中发生错误: " + e.getMessage());
            e.printStackTrace();
        } finally {
            // 13. 关闭SparkContext
            jsc.close();
        }
    }
}

代码解析:分布式计算全流程

1. Spark环境初始化

SparkSession spark = SparkSession.builder()
        .appName("Java Word Count")
        .getOrCreate();

JavaSparkContext jsc = new JavaSparkContext(spark.sparkContext());
  • SparkSession:Spark 2.0+的统一入口点,取代了旧的SparkContext
  • JavaSparkContext:Java API的入口点,用于创建RDD
  • appName("Java Word Count"):设置应用程序名称
  • .master("local[*]"):本地调试时使用,*表示使用所有CPU核心

2. 数据加载:文本文件读取

JavaRDD<String> lines = jsc.textFile(inputPath);
  • textFile()方法从HDFS或本地文件系统读取文本
  • 创建初始RDD(弹性分布式数据集)
  • 每行文本作为RDD中的一个元素

3. 数据处理:词汇转换和清洗

// 拆分单词
JavaRDD<String> words = lines.flatMap(line -> 
    Arrays.asList(line.split("\\s+")).iterator()
);

// 清洗和标准化
JavaRDD<String> cleanedWords = words.map(word -> 
    word.replaceAll("[^a-zA-Z]", "").toLowerCase()
).filter(word -> !word.isEmpty());
  • flatMap:将每行拆分为单词(一个输入项产生多个输出项)
  • map:清理非字母字符并转换为小写(大小写不敏感)
  • filter:过滤掉空字符串

4. MapReduce核心:词频统计

// Map阶段:转换为键值对
JavaPairRDD<String, Integer> wordPairs = cleanedWords.mapToPair(word -> 
    new Tuple2<>(word, 1)
);

// Reduce阶段:相同单词求和
JavaPairRDD<String, Integer> wordCounts = wordPairs.reduceByKey((a, b) -> a + b);
  • mapToPair:将每个单词映射为(word, 1)键值对
  • reduceByKey:分布式聚合操作,对相同key的值进行求和

5. 结果后处理:排序和输出

// 交换键值位置便于排序
JavaPairRDD<Integer, String> countWords = wordCounts.mapToPair(tuple -> 
    new Tuple2<>(tuple._2(), tuple._1())
);

// 按词频降序排序
JavaPairRDD<Integer, String> sortedCounts = countWords.sortByKey(false);

// 恢复原始格式(word, count)
JavaPairRDD<String, Integer> sortedWordCounts = sortedCounts.mapToPair(tuple -> 
    new Tuple2<>(tuple._2(), tuple._1())
);

// 结果输出
sortedWordCounts.saveAsTextFile(outputPath);
  • 通过交换键值对位置,实现按词频值排序
  • sortByKey(false):false表示降序排序
  • saveAsTextFile():将结果保存到输出路径

6. 资源管理:上下文关闭

finally {
    jsc.close();
}
  • 确保无论执行成功与否都关闭SparkContext
  • 释放集群资源

执行方法:编译与运行

1. Maven依赖配置 (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-core_2.12</artifactId>
        <version>3.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-sql_2.12</artifactId>
        <version>3.4.0</version>
    </dependency>
</dependencies>

2. 编译和打包

mvn clean package

3. 提交到Spark集群运行

spark-submit \
  --class SparkWordCount \
  --master yarn \
  --deploy-mode cluster \
  --num-executors 4 \
  --executor-memory 2G \
  --executor-cores 2 \
  target/your-jar-file.jar \
  hdfs:///input/path \
  hdfs:///output/path

4. 本地模式调试

spark-submit \
  --class SparkWordCount \
  --master "local[*]" \
  target/your-jar-file.jar \
  file:///local/input/path \
  file:///local/output/path

高级优化技巧

1. 使用Broadcast变量存储词典

// 创建广播变量
List<String> stopWords = Arrays.asList("a", "an", "the", "and", "or");
Broadcast<List<String>> stopWordsBroadcast = jsc.broadcast(stopWords);

// 在转换中使用
JavaRDD<String> filteredWords = cleanedWords.filter(word -> 
    !stopWordsBroadcast.value().contains(word)
);
  • 适用于需要共享小型数据集的情况
  • 广播到每个Executor,减少网络传输

2. 累加器实现自定义统计

// 创建长整数累加器
LongAccumulator numberAccumulator = jsc.sc().longAccumulator("numberCount");
LongAccumulator specialAccumulator = jsc.sc().longAccumulator("specialCharCount");

// 在转换中使用
JavaRDD<String> cleanedWords = words.map(word -> {
    if (word.matches("\\d+")) {
        numberAccumulator.add(1);
        return "";
    }
    
    if (word.matches(".*[!@#$%^&*()].*")) {
        specialAccumulator.add(1);
    }
    
    return word.replaceAll("[^a-zA-Z]", "").toLowerCase();
});

// 结束后获取累加器值
System.out.println("Numbers count: " + numberAccumulator.value());
System.out.println("Special chars count: " + specialAccumulator.value());

3. 缓存中间结果提高性能

// 缓存高频使用的RDD
wordCounts.persist(StorageLevel.MEMORY_AND_DISK());

// 多种操作使用同一个RDD
JavaPairRDD<String, Integer> filtered = wordCounts.filter(t -> t._2() > 5);
JavaPairRDD<String, Integer> reversed = wordCounts.mapToPair(t -> 
    new Tuple2<>(t._1(), t._2() * -1)
);

// 操作完成后释放
wordCounts.unpersist();

4. 自定义分区器优化负载均衡

import org.apache.spark.Partitioner;

// 自定义首字母分区器
class AlphabetPartitioner extends Partitioner {
    private final int numPartitions;
    
    public AlphabetPartitioner(int partitions) {
        this.numPartitions = partitions;
    }
    
    @Override
    public int numPartitions() {
        return numPartitions;
    }
    
    @Override
    public int getPartition(Object key) {
        String word = (String) key;
        char firstChar = Character.toLowerCase(word.charAt(0));
        if (firstChar >= 'a' && firstChar <= 'z') {
            return firstChar - 'a';
        }
        return 0; // 非字母词分配到0分区
    }
}

// 使用自定义分区器
wordPairs.partitionBy(new AlphabetPartitioner(26))
         .reduceByKey((a, b) -> a + b);

5. 组合操作优化性能

// 使用更高效的组合方法
JavaPairRDD<String, Integer> wordCounts = cleanedWords
    .mapToPair(word -> new Tuple2<>(word, 1))
    .combineByKey(
        // createCombiner: 第一次出现某个键时调用
        count -> count,
        // mergeValue: 该键已经存在时调用
        (count, value) -> count + value,
        // mergeCombiners: 分区合并时调用
        (count1, count2) -> count1 + count2
    );

Spark核心概念解析

RDD(弹性分布式数据集)的核心特性

特性说明在WordCount中的应用
分区数据分布在集群节点上每个分区处理一部分文本行
不可变只读数据集,通过转换生成新RDD每个操作都产生新RDD
容错通过lineage(血统)重建丢失分区任务失败时可重演操作重建数据
延迟执行操作在action触发时执行直到collect()save()才真正执行
内存缓存可持久化到内存加速访问persist()中间结果提升性能

WordCount中的RDD转换流程

初始RDD
flatMap
map
mapToPair
reduceByKey
mapToPair
sortByKey
mapToPair
saveAsTextFile
take
textFile
lines
words
cleanedWords
wordPairs
wordCounts
countWords
sortedCounts
sortedWordCounts
HDFS
Driver

实际应用场景扩展

1. 海量日志分析

// 读取压缩日志文件
JavaRDD<String> logs = jsc.textFile("hdfs:///logs/*.gz");

// 使用正则解析日志
JavaRDD<String> urls = logs.flatMap(line -> {
    Pattern pattern = Pattern.compile("GET (\\S+) ");
    Matcher matcher = pattern.matcher(line);
    List<String> matches = new ArrayList<>();
    while (matcher.find()) {
        matches.add(matcher.group(1));
    }
    return matches.iterator();
});

// 统计URL访问频率
urls.mapToPair(url -> new Tuple2<>(url, 1))
    .reduceByKey((a, b) -> a + b)
    .saveAsTextFile("hdfs:///output/url_counts");

2. 实时流处理词频统计

// 创建流处理上下文
JavaStreamingContext ssc = new JavaStreamingContext(
    jsc, Durations.seconds(5)
);

// 从Kafka创建输入流
JavaInputDStream<ConsumerRecord<String, String>> stream = 
    KafkaUtils.createDirectStream(ssc,
        LocationStrategies.PreferConsistent(),
        ConsumerStrategies.Subscribe(topics, kafkaParams)
    );

// 转换处理
JavaDStream<String> lines = stream.map(ConsumerRecord::value);
JavaDStream<String> words = lines.flatMap(line -> 
    Arrays.asList(line.split("\\s+")).iterator()
);
JavaPairDStream<String, Integer> wordCounts = words
    .mapToPair(word -> new Tuple2<>(word, 1))
    .reduceByKeyAndWindow((a, b) -> a + b, Durations.minutes(5), Durations.minutes(1));

// 输出结果到外部系统
wordCounts.foreachRDD(rdd -> {
    rdd.saveToCassandra("ks", "word_counts");
    return null;
});

ssc.start();
ssc.awaitTermination();

3. Spark SQL词频统计

// 创建DataFrame
Dataset<Row> df = spark.read().textFile(inputPath).toDF("line");

// SQL操作词频统计
df.selectExpr("explode(split(line, ' ')) as word")
  .selectExpr("lower(regexp_replace(word, '[^a-zA-Z]', '')) as cleaned")
  .filter("cleaned != ''")
  .groupBy("cleaned")
  .count()
  .orderBy(functions.desc("count"))
  .write()
  .mode(SaveMode.Overwrite)
  .csv(outputPath);

性能优化指南

WordCount作业调优参数

参数推荐值说明
spark.executor.memory4-8G执行器内存大小
spark.executor.cores2-4每个执行器核心数
spark.executor.instances20-100执行器实例数
spark.default.parallelism总核心数×2-3默认分区数
spark.serializerKryoSerializer高性能序列化
spark.rdd.compresstrue压缩RDD分区
spark.shuffle.service.enabledtrue启用shuffle服务
spark.sql.shuffle.partitions200-2000SQL作业并行度

负载均衡策略

  1. 数据预处理:使用repartition()实现数据均衡分布
    lines.repartition(100) // 分成100个分区
    
  2. 减少Shuffle:避免不必要的groupBy()操作
  3. Join优化
    .reduceByKey(...) // 优于groupByKey
    .mapPartitions(...) // 分区内预处理
    
  4. 内存管理
    // 设置存储内存比例
    spark.memory.storageFraction = 0.6
    // 使用堆外内存
    spark.memory.offHeap.enabled = true
    spark.memory.offHeap.size = "2g"
    

通过这个完整的Word Count示例,您已掌握Spark核心API的使用方法和性能优化策略,为开发复杂大数据应用奠定了坚实基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值