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转换流程
实际应用场景扩展
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.memory | 4-8G | 执行器内存大小 |
spark.executor.cores | 2-4 | 每个执行器核心数 |
spark.executor.instances | 20-100 | 执行器实例数 |
spark.default.parallelism | 总核心数×2-3 | 默认分区数 |
spark.serializer | KryoSerializer | 高性能序列化 |
spark.rdd.compress | true | 压缩RDD分区 |
spark.shuffle.service.enabled | true | 启用shuffle服务 |
spark.sql.shuffle.partitions | 200-2000 | SQL作业并行度 |
负载均衡策略
- 数据预处理:使用
repartition()
实现数据均衡分布lines.repartition(100) // 分成100个分区
- 减少Shuffle:避免不必要的
groupBy()
操作 - Join优化:
.reduceByKey(...) // 优于groupByKey .mapPartitions(...) // 分区内预处理
- 内存管理:
// 设置存储内存比例 spark.memory.storageFraction = 0.6 // 使用堆外内存 spark.memory.offHeap.enabled = true spark.memory.offHeap.size = "2g"
通过这个完整的Word Count示例,您已掌握Spark核心API的使用方法和性能优化策略,为开发复杂大数据应用奠定了坚实基础。