揭開神祕面紗,會stream流就會巨量資料

2023-04-27 18:01:44

如果你會任意一門語言的stream流,沒道理不會巨量資料開發。

俗話說男追女隔座山,女追男隔層紗。
如果說零基礎學巨量資料,感覺前面是一座山,那麼只要你會java或者任意一門語言的stream流,那巨量資料就只隔了一層紗。
本文以java stream流計算為例,講解一些基礎的spark操作。另一個流行的巨量資料框架flink同理。

準備工作

測試資料,以下列分別表示姓名,年齡,部門,職位。

張三,20,研發部,普通員工
李四,31,研發部,普通員工
李麗,36,財務部,普通員工
張偉,38,研發部,經理
杜航,25,人事部,普通員工
周歌,28,研發部,普通員工

建立一個Employee類。

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    static
    class Employee implements Serializable {
        private String name;
        private Integer age;
        private String department;
        private String level;
    }
}

版本:
jdk:1.8
spark:3.2.0
scala:2.12.15
上面的scala版本只是spark框架本身需要依賴到scala。
因為scala確實是比較小眾的語言,本文還是使用java演示spark程式碼。

1.map類

1.1 java stream map

map表示一對一操作。將上游資料的一行資料進行任意操作,最終得到操作後的一條資料。
這種思想,在java和spark,flink都是一致的。

我們先用java stream演示讀取檔案,再使用map操作將每行資料對映為Employee物件。

List<String> list = FileUtils.readLines(new File("f:/test.txt"), "utf-8");
        List<Employee> employeeList = list.stream().map(word -> {
            List<String> words = Arrays.stream(word.split(",")).collect(Collectors.toList());
            Employee employee = new Employee(words.get(0), Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            return employee;
        }).collect(Collectors.toList());

        employeeList.forEach(System.out::println);

轉換後的資料:

JavaStreamDemo.Employee(name=張三, age=20, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=李麗, age=36, department=財務部, level=普通員工)
JavaStreamDemo.Employee(name=張偉, age=38, department=研發部, level=經理)
JavaStreamDemo.Employee(name=杜航, age=25, department=人事部, level=普通員工)
JavaStreamDemo.Employee(name=周歌, age=28, department=研發部, level=普通員工)

1.2 spark map

首先得到一個SparkSession物件,讀取檔案,得到一個DataSet彈性資料集物件。

SparkSession session = SparkSession.builder().master("local[*]").getOrCreate();
Dataset<Row> reader = session.read().text("F:/test.txt");
reader.show();

這裡的show()就是列印輸出當前資料集,它是一個action類的運算元。
得到結果:

+-----------------------+
|                  value|
+-----------------------+
|張三,20,研發部,普通員工|
|李四,31,研發部,普通員工|
|李麗,36,財務部,普通員工|
|    張偉,38,研發部,經理|
|杜航,25,人事部,普通員工|
|周歌,28,研發部,普通員工|
+-----------------------+

現在我們拿到了基礎資料,我們使用map一對一操作,將一行行資料轉換為Employee物件。
我們這裡不使用lamda表示式,讓大家看得更加清晰。
這裡實現了MapFunction介面裡的call方法,每次拿到一行資料,我們這裡進行切分,再轉換為物件。

  1. 需要特別指出的一點是,與後端WEB應用有一個統一例外處理不同的是,巨量資料應用,特別是流式計算,要保證7*24線上,需要對每個運算元進行異常捕獲。
    因為你不知道上游資料淨化到底怎麼樣,很可能拿到一條髒資料,處理的時候丟擲異常,如果沒有捕獲處理,那麼整個應用就會掛掉。

  2. spark的運算元分為Transformation和Action兩種型別。Transformation會開成一個DAG圖,具有lazy延遲性,它只會從一個dataset(rdd/df)轉換成另一個dataset(rdd/df),只有當遇到action類的運算元才會真正執行。
    我們今天會演示的運算元都是Transformation類的運算元。
    典型的Action運算元包括show,collect,save之類的。比如在本地進行show檢視結果,或者完成執行後save到資料庫,或者HDFS。

  3. spark執行時分為driver和executor。但不是本文的重點,不會展開講。
    只需要注意driver端會將程式碼分發到各個分散式系統的節點executor上,它本身不會參與計算。一般來說,運算元外部,如以下範例程式碼的a處會在driver端執行,b處運算元內部會不同伺服器上的executor端執行。
    所以在運算元外部定義的變數,在運算元內部使用的時候要特別注意!! 不要想當然地以為都是一個main方法裡寫的程式碼,就一定會在同一個JVM裡。
    這裡涉及到序列化的問題,同時它們分處不同的JVM,使用"=="比較的時候也可能會出問題!!
    這是一個後端WEB開發轉向巨量資料開發時,這個思想一定要轉變過來。
    簡言之,後端WEB服務的分散式是我們自己實現的,巨量資料的分散式是框架天生幫我們實現的

1.2.1 MapFunction

// a 運算元外部,driver端
Dataset<Employee> employeeDataset = reader.map(new MapFunction<Row, Employee>() {
            @Override
            public Employee call(Row row) throws Exception {
                // b 運算元內部,executor端
                Employee employee = null;
                try {
                    // gson.fromJson(); 這裡使用gson涉及到序列化問題
                    List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                    employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                } catch (Exception exception) {
                    // 紀錄檔記錄
                    // 流式計算中要做到7*24小時不間斷,任意一條上流髒資料都可能導致失敗,從而導致任務退出,所以這裡要做好異常的抓取
                    exception.printStackTrace();
                }
                return employee;
            }
        }, Encoders.bean(Employee.class));

        employeeDataset.show();

輸出

+---+----------+--------+----+
|age|department|   level|name|
+---+----------+--------+----+
| 20|    研發部|普通員工|張三|
| 31|    研發部|普通員工|李四|
| 36|    財務部|普通員工|李麗|
| 38|    研發部|    經理|張偉|
| 25|    人事部|普通員工|杜航|
| 28|    研發部|普通員工|周歌|

1.2.2 MapPartitionsFunction

spark中 map和mapPartitions有啥區別?
map是1條1條處理資料
mapPartitions是一個分割區一個分割區處理資料


後者一定比前者效率高嗎?
不一定,看具體情況。

這裡使用前面 map 一樣的邏輯處理。可以看到在call方法裡得到的是一個Iterator迭代器,是一批資料。
得到一批資料,然後再一對一對映為物件,再以Iterator的形式返回這批資料。

Dataset<Employee> employeeDataset2 = reader.mapPartitions(new MapPartitionsFunction<Row, Employee>() {
            @Override
            public Iterator<Employee> call(Iterator<Row> iterator) throws Exception {
                List<Employee> employeeList = new ArrayList<>();
                while (iterator.hasNext()){
                    Row row = iterator.next();
                    try {
                        List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                        Employee employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                        employeeList.add(employee);
                    } catch (Exception exception) {
                        // 紀錄檔記錄
                        // 流式計算中要做到7*24小時不間斷,任意一條上流髒資料都可能導致失敗,從而導致任務退出,所以這裡要做好異常的抓取
                        exception.printStackTrace();
                    }
                }
                return employeeList.iterator();
            }
        }, Encoders.bean(Employee.class));

        employeeDataset2.show();

輸出結果跟map一樣,這裡就不貼出來了。

2.flatMap類

map和flatMap有什麼區別?
map是一對一,flatMap是一對多。
當然在java stream中,flatMap叫法叫做扁平化。

這種思想,在java和spark,flink都是一致的。

2.1 java stream flatMap

以下程式碼將1條原始資料對映到2個物件上並返回。

List<Employee> employeeList2 = list.stream().flatMap(word -> {
            List<String> words = Arrays.stream(word.split(",")).collect(Collectors.toList());
            List<Employee> lists = new ArrayList<>();
            Employee employee = new Employee(words.get(0), Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            lists.add(employee);
            Employee employee2 = new Employee(words.get(0)+"_2", Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            lists.add(employee2);
            return lists.stream();
        }).collect(Collectors.toList());
        employeeList2.forEach(System.out::println);

輸出

JavaStreamDemo.Employee(name=張三, age=20, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=張三_2, age=20, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=李四_2, age=31, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=李麗, age=36, department=財務部, level=普通員工)
JavaStreamDemo.Employee(name=李麗_2, age=36, department=財務部, level=普通員工)
JavaStreamDemo.Employee(name=張偉, age=38, department=研發部, level=經理)
JavaStreamDemo.Employee(name=張偉_2, age=38, department=研發部, level=經理)
JavaStreamDemo.Employee(name=杜航, age=25, department=人事部, level=普通員工)
JavaStreamDemo.Employee(name=杜航_2, age=25, department=人事部, level=普通員工)
JavaStreamDemo.Employee(name=周歌, age=28, department=研發部, level=普通員工)
JavaStreamDemo.Employee(name=周歌_2, age=28, department=研發部, level=普通員工)

2.2 spark flatMap

這裡實現FlatMapFunction的call方法,一次拿到1條資料,然後返回值是Iterator,所以可以返回多條。

Dataset<Employee> employeeDatasetFlatmap = reader.flatMap(new FlatMapFunction<Row, Employee>() {
            @Override
            public Iterator<Employee> call(Row row) throws Exception {
                List<Employee> employeeList = new ArrayList<>();
                try {
                    List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                    Employee employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                    employeeList.add(employee);

                    Employee employee2 = new Employee(list.get(0)+"_2", Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                    employeeList.add(employee2);
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
                return employeeList.iterator();
            }
        }, Encoders.bean(Employee.class));
        employeeDatasetFlatmap.show();

輸出

+---+----------+--------+------+
|age|department|   level|  name|
+---+----------+--------+------+
| 20|    研發部|普通員工|  張三|
| 20|    研發部|普通員工|張三_2|
| 31|    研發部|普通員工|  李四|
| 31|    研發部|普通員工|李四_2|
| 36|    財務部|普通員工|  李麗|
| 36|    財務部|普通員工|李麗_2|
| 38|    研發部|    經理|  張偉|
| 38|    研發部|    經理|張偉_2|
| 25|    人事部|普通員工|  杜航|
| 25|    人事部|普通員工|杜航_2|
| 28|    研發部|普通員工|  周歌|
| 28|    研發部|普通員工|周歌_2|
+---+----------+--------+------+

3 groupby類

與SQL類似,java stream流和spark一樣,groupby對資料集進行分組並在此基礎上可以進行聚合函數操作。也可以分組直接得到一組子資料集。

3.1 java stream groupBy

按部門分組統計部門人數:

Map<String, Long> map = employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));
        System.out.println(map);

輸出

{財務部=1, 人事部=1, 研發部=4}

3.2 spark groupBy

將對映為物件的資料集按部門分組,在此基礎上統計部門員工數和平均年齡。

RelationalGroupedDataset datasetGroupBy = employeeDataset.groupBy("department");
// 統計每個部門有多少員工
datasetGroupBy.count().show(); 
/**
 * 每個部門的平均年齡
 */
datasetGroupBy.avg("age").withColumnRenamed("avg(age)","avgAge").show();

輸出分別為

+----------+-----+
|department|count|
+----------+-----+
|    財務部|    1|
|    人事部|    1|
|    研發部|    4|
+----------+-----+
+----------+------+
|department|avgAge|
+----------+------+
|    財務部|  36.0|
|    人事部|  25.0|
|    研發部| 29.25|
+----------+------+

3.3 spark groupByKey

spark的groupBygroupByKey的區別,前者在此基礎上使用聚合函數得到一個聚合值,後者只是進行分組,不進行任何計算。
類似於java stream的:

Map<String, List<Employee>> map2 = employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println(map2);

輸出

{財務部=[JavaStreamDemo.Employee(name=李麗, age=36, department=財務部, level=普通員工)], 
人事部=[JavaStreamDemo.Employee(name=杜航, age=25, department=人事部, level=普通員工)], 
研發部=[
JavaStreamDemo.Employee(name=張三, age=20, department=研發部, level=普通員工), 
JavaStreamDemo.Employee(name=李四, age=31, department=研發部, level=普通員工), 
JavaStreamDemo.Employee(name=張偉, age=38, department=研發部, level=經理), 
JavaStreamDemo.Employee(name=周歌, age=28, department=研發部, level=普通員工)]}

使用spark groupByKey。
先得到一個key-value的一對多的一個集合資料集。
這裡的call()方法返回的是key,即分組的key。

KeyValueGroupedDataset keyValueGroupedDataset = employeeDataset.groupByKey(new MapFunction<Employee, String>() {
            @Override
            public String call(Employee employee) throws Exception {
                // 返回分組的key,這裡表示根據部門進行分組
                return employee.getDepartment();
            }
        }, Encoders.STRING());

再在keyValueGroupedDataset 的基礎上進行mapGroups,在call()方法裡就可以拿到每個key的所有原始資料。

keyValueGroupedDataset.mapGroups(new MapGroupsFunction() {
            @Override
            public Object call(Object key, Iterator iterator) throws Exception {
                System.out.println("key = " + key);
                while (iterator.hasNext()){
                    System.out.println(iterator.next());
                }
                return iterator; 
            }
        }, Encoders.bean(Iterator.class))
                .show(); // 這裡的show()沒有意義,只是觸發計算而已

輸出

key = 人事部
SparkDemo.Employee(name=杜航, age=25, department=人事部, level=普通員工)
key = 研發部
SparkDemo.Employee(name=張三, age=20, department=研發部, level=普通員工)
SparkDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)
SparkDemo.Employee(name=張偉, age=38, department=研發部, level=經理)
SparkDemo.Employee(name=周歌, age=28, department=研發部, level=普通員工)
key = 財務部
SparkDemo.Employee(name=李麗, age=36, department=財務部, level=普通員工)

4 reduce類

reduce的字面意思是:減少;減小;降低;縮小。
又叫歸約。

它將資料集進行迴圈,讓當前物件前一物件兩兩進行計算,每次計算得到的結果作為下一次計算的前一物件,並最終得到一個物件。
假設有5個資料【1,2,3,4,5】,使用reduce進行求和計算,分別是

比如上面的測試資料集,我要計算各部門年齡總數。使用聚合函數得到的是一個int型別的數位。

4.1 java stream reduce

int age = employeeList.stream().mapToInt(e -> e.age).sum();
System.out.println(age);//178

使用reduce也可進行上面的計算

int age1 = employeeList.stream().mapToInt(e -> e.getAge()).reduce(0,(a,b) -> a+b);
System.out.println(age1);// 178

但是我將年齡求和,同時得到一個完整的物件呢?

JavaStreamDemo.Employee(name=周歌, age=178, department=研發部, level=普通員工)

可以使用reduce將資料集兩兩回圈,將年齡相加,同時返回最後一個遍歷的物件。
下面程式碼的pre 代表前一個物件,current 代表當前物件。

 /**
 * pre 代表前一個物件
 * current 代表當前物件
 */
Employee reduceEmployee = employeeList.stream().reduce(new Employee(), (pre,current) -> {
     // 當第一次迴圈時前一個物件為null
    if (pre.getAge() == null) {
        current.setAge(current.getAge());
    } else {
        current.setAge(pre.getAge() + current.getAge());
    }
    return current;
});
System.out.println(reduceEmployee);

4.2 spark reduce

spark reduce的基本思想跟java stream是一樣的。
直接看程式碼:

 Employee datasetReduce = employeeDataset.reduce(new ReduceFunction<Employee>() {
            @Override
            public Employee call(Employee t1, Employee t2) throws Exception {
                // 不同的版本看是否需要判斷t1 == null
                t2.setAge(t1.getAge() + t2.getAge());
                return t2;
            }
        });

        System.out.println(datasetReduce);

輸出

SparkDemo.Employee(name=周歌, age=178, department=研發部, level=普通員工)

其它常見操作類

Employee employee = employeeDataset.filter("age > 30").limit(3).sort("age").first();
System.out.println(employee);
// SparkDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)

同時可以將dataset註冊成table,使用更為強大的SQL來進行各種強大的運算。
現在SQL是flink的一等公民,spark也不遑多讓。
這裡舉一個非常簡單的例子。

employeeDataset.registerTempTable("table");
session.sql("select * from table where age > 30 order by age desc limit 3").show();

輸出

+---+----------+--------+----+
|age|department|   level|name|
+---+----------+--------+----+
| 38|    研發部|    經理|張偉|
| 36|    財務部|普通員工|李麗|
| 31|    研發部|普通員工|李四|
+---+----------+--------+----+
employeeDataset.registerTempTable("table");
session.sql("select 
            concat_ws(',',collect_set(name)) as names, // group_concat
            avg(age) as age,
            department from table 
            where age > 30  
            group by department 
            order by age desc 
            limit 3").show();

輸出

+---------+----+----------+
|    names| age|department|
+---------+----+----------+
|     李麗|36.0|    財務部|
|張偉,李四|34.5|    研發部|
+---------+----+----------+

小結

本文依據java stream的相似性,介紹了spark裡面一些常見的運算元操作。
本文只是做一個非常簡單的入門介紹。
如果感興趣的話,
後端的同學可以嘗試著操作一下,非常簡單,本地不需要搭建環境,只要引入spark 的 maven依賴即可。
我把本文的所有程式碼全部貼在最後面。

java stream 原始碼:

點選檢視程式碼
import lombok.*;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class JavaStreamDemo {
    public static void main(String[] args) throws IOException {
        /**
         * 張三,20,研發部,普通員工
         * 李四,31,研發部,普通員工
         * 李麗,36,財務部,普通員工
         * 張偉,38,研發部,經理
         * 杜航,25,人事部,普通員工
         * 周歌,28,研發部,普通員工
         */
        List<String> list = FileUtils.readLines(new File("f:/test.txt"), "utf-8");
        List<Employee> employeeList = list.stream().map(word -> {
            List<String> words = Arrays.stream(word.split(",")).collect(Collectors.toList());
            Employee employee = new Employee(words.get(0), Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            return employee;
        }).collect(Collectors.toList());

        // employeeList.forEach(System.out::println);

        List<Employee> employeeList2 = list.stream().flatMap(word -> {
            List<String> words = Arrays.stream(word.split(",")).collect(Collectors.toList());
            List<Employee> lists = new ArrayList<>();
            Employee employee = new Employee(words.get(0), Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            lists.add(employee);
            Employee employee2 = new Employee(words.get(0)+"_2", Integer.parseInt(words.get(1)), words.get(2), words.get(3));
            lists.add(employee2);
            return lists.stream();
        }).collect(Collectors.toList());
        // employeeList2.forEach(System.out::println);

        Map<String, Long> map = employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));
        System.out.println(map);
        Map<String, List<Employee>> map2 = employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment));
        System.out.println(map2);

        int age = employeeList.stream().mapToInt(e -> e.age).sum();
        System.out.println(age);// 178

        int age1 = employeeList.stream().mapToInt(e -> e.getAge()).reduce(0,(a,b) -> a+b);
        System.out.println(age1);// 178

        /**
         * pre 代表前一個物件
         * current 代表當前物件
         */
        Employee reduceEmployee = employeeList.stream().reduce(new Employee(), (pre,current) -> {
            if (pre.getAge() == null) {
                current.setAge(current.getAge());
            } else {
                current.setAge(pre.getAge() + current.getAge());
            }
            return current;
        });
        System.out.println(reduceEmployee);




    }

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    static
    class Employee implements Serializable {
        private String name;
        private Integer age;
        private String department;
        private String level;
    }
}

spark的原始碼:

點選檢視程式碼
import com.google.gson.Gson;
import lombok.*;
import org.apache.spark.api.java.function.*;
import org.apache.spark.sql.*;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @projectName: spark-demo
 * @package: com.alpha.data
 * @className: SparkDemo
 * @author: nyp
 * @description: TODO
 * @date: 2023/4/27 9:06
 * @version: 1.0
 */
public class SparkDemo {
    public static void main(String[] args) {
        SparkSession session = SparkSession.builder().master("local[*]").getOrCreate();
        Dataset<Row> reader = session.read().text("F:/test.txt");
        // reader.show();
        /**
         * +-----------------------+
         * |                  value|
         * +-----------------------+
         * |張三,20,研發部,普通員工|
         * |李四,31,研發部,普通員工|
         * |李麗,36,財務部,普通員工|
         * |張偉,38,研發部,經理|
         * |杜航,25,人事部,普通員工|
         * |周歌,28,研發部,普通員工|
         * +-----------------------+
         */

        // 本地演示而已,實際分散式環境,這裡的gson涉及到序列化問題
        // 運算元以外的程式碼都在driver端執行
        // 任何運算元以內的程式碼都在executor端執行,即會在不同的伺服器節點上執行
        Gson gson = new Gson();
        // a 運算元外部,driver端
        Dataset<Employee> employeeDataset = reader.map(new MapFunction<Row, Employee>() {
            @Override
            public Employee call(Row row) throws Exception {
                // b 運算元內部,executor端
                Employee employee = null;
                try {
                    // gson.fromJson(); 這裡使用gson涉及到序列化問題
                    List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                    employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                } catch (Exception exception) {
                    // 紀錄檔記錄
                    // 流式計算中要做到7*24小時不間斷,任意一條上流髒資料都可能導致失敗,從而導致任務退出,所以這裡要做好異常的抓取
                    exception.printStackTrace();
                }
                return employee;
            }
        }, Encoders.bean(Employee.class));

        // employeeDataset.show();
        /**
         * +---+----------+--------+----+
         * |age|department|   level|name|
         * +---+----------+--------+----+
         * | 20|    研發部|普通員工|張三|
         * | 31|    研發部|普通員工|李四|
         * | 36|    財務部|普通員工|李麗|
         * | 38|    研發部|    經理|張偉|
         * | 25|    人事部|普通員工|杜航|
         * | 28|    研發部|普通員工|周歌|
         */

        Dataset<Employee> employeeDataset2 = reader.mapPartitions(new MapPartitionsFunction<Row, Employee>() {
            @Override
            public Iterator<Employee> call(Iterator<Row> iterator) throws Exception {
                List<Employee> employeeList = new ArrayList<>();
                while (iterator.hasNext()){
                    Row row = iterator.next();
                    try {
                        List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                        Employee employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                        employeeList.add(employee);
                    } catch (Exception exception) {
                        // 紀錄檔記錄
                        // 流式計算中要做到7*24小時不間斷,任意一條上流髒資料都可能導致失敗,從而導致任務退出,所以這裡要做好異常的抓取
                        exception.printStackTrace();
                    }
                }
                return employeeList.iterator();
            }
        }, Encoders.bean(Employee.class));

        // employeeDataset2.show();
        /**
         * +---+----------+--------+----+
         * |age|department|   level|name|
         * +---+----------+--------+----+
         * | 20|    研發部|普通員工|張三|
         * | 31|    研發部|普通員工|李四|
         * | 36|    財務部|普通員工|李麗|
         * | 38|    研發部|    經理|張偉|
         * | 25|    人事部|普通員工|杜航|
         * | 28|    研發部|普通員工|周歌|
         * +---+----------+--------+----+
         */

        Dataset<Employee> employeeDatasetFlatmap = reader.flatMap(new FlatMapFunction<Row, Employee>() {
            @Override
            public Iterator<Employee> call(Row row) throws Exception {
                List<Employee> employeeList = new ArrayList<>();
                try {
                    List<String> list = Arrays.stream(row.mkString().split(",")).collect(Collectors.toList());
                    Employee employee = new Employee(list.get(0), Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                    employeeList.add(employee);

                    Employee employee2 = new Employee(list.get(0)+"_2", Integer.parseInt(list.get(1)), list.get(2), list.get(3));
                    employeeList.add(employee2);
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
                return employeeList.iterator();
            }
        }, Encoders.bean(Employee.class));
//        employeeDatasetFlatmap.show();
        /**
         * +---+----------+--------+------+
         * |age|department|   level|  name|
         * +---+----------+--------+------+
         * | 20|    研發部|普通員工|  張三|
         * | 20|    研發部|普通員工|張三_2|
         * | 31|    研發部|普通員工|  李四|
         * | 31|    研發部|普通員工|李四_2|
         * | 36|    財務部|普通員工|  李麗|
         * | 36|    財務部|普通員工|李麗_2|
         * | 38|    研發部|    經理|  張偉|
         * | 38|    研發部|    經理|張偉_2|
         * | 25|    人事部|普通員工|  杜航|
         * | 25|    人事部|普通員工|杜航_2|
         * | 28|    研發部|普通員工|  周歌|
         * | 28|    研發部|普通員工|周歌_2|
         * +---+----------+--------+------+
         */

        RelationalGroupedDataset datasetGroupBy = employeeDataset.groupBy("department");
        // 統計每個部門有多少員工
        // datasetGroupBy.count().show();
        /**
         * +----------+-----+
         * |department|count|
         * +----------+-----+
         * |    財務部|    1|
         * |    人事部|    1|
         * |    研發部|    4|
         * +----------+-----+
         */
        /**
         * 每個部門的平均年齡
         */
        // datasetGroupBy.avg("age").withColumnRenamed("avg(age)","avgAge").show();
        /**
         * +----------+--------+
         * |department|avg(age)|
         * +----------+--------+
         * |    財務部|    36.0|
         * |    人事部|    25.0|
         * |    研發部|   29.25|
         * +----------+--------+
         */

        KeyValueGroupedDataset keyValueGroupedDataset = employeeDataset.groupByKey(new MapFunction<Employee, String>() {
            @Override
            public String call(Employee employee) throws Exception {
                // 返回分組的key,這裡表示根據部門進行分組
                return employee.getDepartment();
            }
        }, Encoders.STRING());

        keyValueGroupedDataset.mapGroups(new MapGroupsFunction() {
            @Override
            public Object call(Object key, Iterator iterator) throws Exception {
                System.out.println("key = " + key);
                while (iterator.hasNext()){
                    System.out.println(iterator.next());
                }
                return iterator;
                /**
                 * key = 人事部
                 * SparkDemo.Employee(name=杜航, age=25, department=人事部, level=普通員工)
                 * key = 研發部
                 * SparkDemo.Employee(name=張三, age=20, department=研發部, level=普通員工)
                 * SparkDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)
                 * SparkDemo.Employee(name=張偉, age=38, department=研發部, level=經理)
                 * SparkDemo.Employee(name=周歌, age=28, department=研發部, level=普通員工)
                 * key = 財務部
                 * SparkDemo.Employee(name=李麗, age=36, department=財務部, level=普通員工)
                 */
            }
        }, Encoders.bean(Iterator.class))
                .show(); // 這裡的show()沒有意義,只是觸發計算而已


        Employee datasetReduce = employeeDataset.reduce(new ReduceFunction<Employee>() {
            @Override
            public Employee call(Employee t1, Employee t2) throws Exception {
                // 不同的版本看是否需要判斷t1 == null
                t2.setAge(t1.getAge() + t2.getAge());
                return t2;
            }
        });

        System.out.println(datasetReduce);


        Employee employee = employeeDataset.filter("age > 30").limit(3).sort("age").first();
        System.out.println(employee);
        // SparkDemo.Employee(name=李四, age=31, department=研發部, level=普通員工)

        employeeDataset.registerTempTable("table");
        session.sql("select * from table where age > 30 order by age desc limit 3").show();

        /**
         * +---+----------+--------+----+
         * |age|department|   level|name|
         * +---+----------+--------+----+
         * | 38|    研發部|    經理|張偉|
         * | 36|    財務部|普通員工|李麗|
         * | 31|    研發部|普通員工|李四|
         * +---+----------+--------+----+
         */


    }

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public static class Employee implements Serializable {
        private String name;
        private Integer age;
        private String department;
        private String level;
    }
}

spark maven依賴,自行不需要的spark-streaming,kafka依賴去掉。

點選檢視程式碼
<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <scala.version>2.12.15</scala.version>
        <spark.version>3.2.0</spark.version>
        <encoding>UTF-8</encoding>
    </properties>
    <dependencies>
        <!-- scala依賴-->
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>
        <!-- spark依賴-->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_2.12</artifactId>
            <version>${spark.version}</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
            <scope>provided</scope>
        </dependency>

        <!--<dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>
            <version>${spark.version}</version>
        </dependency>-->

        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql-kafka-0-10_2.12</artifactId>
            <version>${spark.version}</version>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.7</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>

    </dependencies>