<think>
我们遇到了一个异常:`org.apache.flink.api.common.InvalidProgramException: The implementation of the RichOutputFormat is not serializable. The object probably contains or references non serializable fields.`
这个异常通常是由于在Flink的算子中使用了非可序列化的对象。在我们的代码中,我们使用了`JdbcSink.sink`,它可能包含了一些非可序列化的部分(比如`JdbcConnectionOptions`),而Flink需要将整个算子序列化以分发到各个TaskManager上执行。
具体来看,我们的代码中,`JdbcSink.sink`方法返回的sink function可能包含了一个`RichOutputFormat`(或者类似的内部类),而我们在`addSink`方法中直接使用了这个sink function。但是,在构建这个sink function时,我们传入了一个lambda表达式和一个`JdbcConnectionOptions`对象。如果这个lambda表达式捕获了任何非可序列化的对象,或者`JdbcConnectionOptions`本身不可序列化,就会导致问题。
然而,根据Flink的文档,`JdbcConnectionOptions`是可序列化的,所以问题可能出在lambda表达式上。在Scala中,lambda表达式有时会捕获外部的非可序列化对象,但在这个例子中,我们的lambda表达式只引用了`tuple`,它是可序列化的。
但是,仔细看异常堆栈,它提到“Non-serializable lambda”,所以可能是Flink在序列化整个sink function时,lambda表达式无法被正确序列化。
解决方案:
1. 我们可以尝试将这个lambda表达式提取出来,变成一个可序列化的类(比如一个实现了`Serializable`接口的类)。
2. 另一种方法是使用Flink的`RichSinkFunction`来重写这个sink。
这里我们选择第二种方法,因为`JdbcSink`内部已经处理了连接,我们也可以选择使用`RichSinkFunction`来手动管理连接。但是,Flink提供了`JdbcSink`作为工具方法,我们更希望直接使用它。因此,我们尝试将构建sink的部分包装在一个可序列化的类中。
但是,我们也可以尝试另一种方法:确保lambda表达式不捕获任何外部不可序列化的对象。在我们的代码中,lambda表达式只使用了传入的参数(tuple)和已经定义好的常量(如连接选项),连接选项在构建时已经传入,并且它应该是可序列化的。
为什么还会报错呢?可能是因为在构建过程中,这个lambda表达式被转换成了一个内部类,而内部类隐式捕获了外部类的引用(即`FlinkWriteToMySQL$`对象),而外部类是不可序列化的(因为Scala对象默认是可序列化的吗?实际上,Scala的object是单例,通常不建议序列化整个object)。所以,我们需要避免lambda捕获外部类的引用。
我们可以通过将连接选项定义为本地变量,并且确保lambda表达式不引用任何外部类的成员(即不引用`this`)来解决。但是我们的代码中,lambda表达式并没有引用外部类的成员啊?实际上,在Scala中,如果我们使用的是在方法内部定义的局部变量,那么lambda表达式就不会捕获外部类。
我们目前的代码中,连接选项的构建是在`addSink`方法内部完成的,所以lambda表达式只捕获了方法内部的变量(即连接选项和SQL字符串),这些变量在构建时已经创建,并且连接选项是可序列化的。
那么问题可能出在Scala的lambda表达式的序列化机制上。我们可以尝试将lambda表达式替换为一个显式的函数类,并确保这个类是可序列化的。
根据Flink官方文档,建议对于这种问题,可以使用显式的类来代替lambda表达式。
我们修改如下:
将:
```scala
(ps: PreparedStatement, tuple: (Int, String)) => {
ps.setInt(1, tuple._1)
ps.setString(2, tuple._2)
}
```
替换为一个实现了`JdbcStatementBuilder`并且可序列化的类。
修改后代码(主要修改部分):
```scala
// 定义一个可序列化的类
class StudentJdbcStatementBuilder extends JdbcStatementBuilder[(Int, String)] with Serializable {
override def accept(ps: PreparedStatement, t: (Int, String)): Unit = {
ps.setInt(1, t._1)
ps.setString(2, t._2)
}
}
// 然后在sink中使用这个类的实例
tupleStream.addSink(
JdbcSink.sink[(Int, String)](
"INSERT INTO student (Id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)",
new StudentJdbcStatementBuilder,
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC")
.withDriverName("com.mysql.cj.jdbc.Driver")
.withUsername("root")
.withPassword("12345678")
.withConnectionCheckTimeoutSeconds(5)
.build
)
)
```
这样,我们避免了使用lambda表达式,而是使用了一个显式的可序列化的类。
另外,我们还需要注意,整个`FlinkWriteToMySQL$`对象(Scala的object)在序列化时会被捕获吗?在`addSink`方法中,我们使用的是本地变量,所以不会捕获外部对象。但是,如果我们的`StudentJdbcStatementBuilder`是一个内部类,那么它就会捕获外部类的引用(即`FlinkWriteToMySQL$`)。因此,为了避免这个问题,我们应该将这个类定义在方法内部(局部内部类)或者定义为顶级类。但是Scala中,我们可以在方法内部定义内部类吗?可以,但是这样这个内部类就会捕获方法所在对象的引用。所以我们最好将其定义在对象内部,并标记为`static`(在Scala中,内部类默认是依赖外部类实例的,除非我们将其定义为`class`在另一个对象中或者使用`@SerialVersionUID`并确保不引用外部类)。
然而,Scala中没有静态内部类,我们可以将`StudentJdbcStatementBuilder`定义在`FlinkWriteToMySQL`对象内部,但是不捕获外部成员。只要它不引用外部类的任何成员,那么即使捕获了外部类的引用,外部类(object)也是可序列化的(因为Scala对象在序列化时会被当作单例处理,但反序列化时可能会有问题)。所以为了安全起见,我们可以将这个类定义在方法内部,这样它只会捕获方法内的局部变量(如果方法内有局部变量被使用,则同样会有问题)。或者,我们将其定义在伴生对象的外部,作为一个独立的类。
由于我们只是一个小例子,为了方便,我们可以直接定义在对象内部,并确保它不引用外部类的成员(即不使用`FlinkWriteToMySQL`的任何成员变量或方法)。
修改后的完整代码:
```scala
import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, createTypeInformation}
import org.apache.flink.connector.jdbc.{JdbcSink, JdbcConnectionOptions, JdbcStatementBuilder}
import java.sql.PreparedStatement
object FlinkWriteToMySQL {
// 定义在对象内部的内部类
class StudentJdbcStatementBuilder extends JdbcStatementBuilder[(Int, String)] with Serializable {
override def accept(ps: PreparedStatement, t: (Int, String)): Unit = {
ps.setInt(1, t._1)
ps.setString(2, t._2)
}
}
def main(args: Array[String]): Unit = {
val namePrefix = "luyifan" // 修改为你的名字拼音
val env = StreamExecutionEnvironment.getExecutionEnvironment
val studentData = Seq(
s"100,${namePrefix}100",
s"101,${namePrefix}101",
s"102,${namePrefix}102_Math",
s"103,${namePrefix}103_CS",
s"104,${namePrefix}104_Physics"
)
val dataStream = env.fromCollection(studentData)
val tupleStream = dataStream.map { line =>
val parts = line.split(",", 2)
(parts(0).trim.toInt, parts(1).trim)
}
tupleStream.addSink(
JdbcSink.sink(
"INSERT INTO student (Id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)",
new StudentJdbcStatementBuilder, // 使用内部类的实例
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC")
.withDriverName("com.mysql.cj.jdbc.Driver")
.withUsername("root")
.withPassword("12345678")
.withConnectionCheckTimeoutSeconds(5)
.build
)
)
println(s"开始写入 ${studentData.size} 条学生数据到MySQL...")
env.execute("Flink Write to MySQL - luyifan")
}
}
```
注意:这里我们将`StudentJdbcStatementBuilder`定义在了`FlinkWriteToMySQL`对象内部,并且它没有引用外部对象的任何成员(即没有使用`FlinkWriteToMySQL`的字段或方法),所以即使它持有对外部对象的引用,外部对象(单例)在序列化时也是安全的(因为单例对象在反序列化时并不会真的反序列化,而是返回已有的单例)。但是,为了确保万无一失,我们也可以将这个类定义在方法内部(即`main`方法内部),这样它就不会捕获外部对象。修改如下:
```scala
def main(args: Array[String]): Unit = {
// ... 其他代码 ...
// 在main方法内部定义内部类
class StudentJdbcStatementBuilder extends JdbcStatementBuilder[(Int, String)] with Serializable {
override def accept(ps: PreparedStatement, t: (Int, String)): Unit = {
ps.setInt(1, t._1)
ps.setString(2, t._2)
}
}
tupleStream.addSink(
JdbcSink.sink(
"INSERT INTO student (Id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)",
new StudentJdbcStatementBuilder, // 使用内部类的实例
// ... 连接选项 ...
)
)
}
```
但是,在方法内部定义类,这个类会捕获方法所在对象的引用(即`FlinkWriteToMySQL$`),所以还是会有同样的问题。因此,我们更推荐将这个类定义在对象外部(作为顶级类),或者使用匿名内部类并确保不引用外部成员(实际上,匿名内部类也会捕获外部类引用)。
所以,最安全的方式是将其定义为一个顶级类。但是为了代码简洁,我们可以将其定义在`FlinkWriteToMySQL`对象内部,并加上`@SerialVersionUID`注解,并确保它不引用外部类的成员。因为实际上它没有引用,所以应该是安全的。
另外,注意在Flink中,通常要求所有在算子中使用的函数都是可序列化的。我们这里使用内部类,只要确保内部类是静态的(在Scala中,没有静态内部类,但是嵌套类不持有外部类的引用,除非使用`outer`)。实际上,在Scala中,定义在对象内部的类默认就是静态的(类似于Java的静态内部类),所以不会持有外部类实例的引用(因为对象本身就是单例,没有实例)。所以,这里定义在对象内部是安全的。
因此,我们采用第一种修改方式:将`StudentJdbcStatementBuilder`定义在`FlinkWriteToMySQL`对象内部,作为一个嵌套类。
运行修改后的代码,应该可以避免序列化异常。
另外,我们还看到程序输出了关于SLF4J和log4j的警告,这可能会导致日志无法输出。我们可以添加log4j的配置文件来解决。在资源目录下添加`log4j.properties`文件,内容如下:
```
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p %-60c %x - %m%n
```
这样,日志就可以正常输出了。
综上所述,修改后的完整代码如下:
</think>
### 解决 Flink 序列化异常问题
#### 问题分析:
异常信息表明 `RichOutputFormat` 实现不可序列化,具体原因是 lambda 表达式 `FlinkWriteToMySQL$$$Lambda$92` 不可序列化。在 Flink 中,所有需要在任务管理器之间传输的函数(包括 Sink)必须实现 `Serializable` 接口。
#### 解决方案:
需要将 lambda 表达式替换为显式实现 `JdbcStatementBuilder` 接口的序列化类:
```scala
import org.apache.flink.streaming.api.scala.{StreamExecutionEnvironment, createTypeInformation}
import org.apache.flink.connector.jdbc.{JdbcSink, JdbcConnectionOptions, JdbcStatementBuilder}
import java.sql.PreparedStatement
import java.io.Serializable
// 显式实现可序列化的语句构建器
class SerializableStatementBuilder extends JdbcStatementBuilder[(Int, String)] with Serializable {
override def accept(ps: PreparedStatement, tuple: (Int, String)): Unit = {
ps.setInt(1, tuple._1)
ps.setString(2, tuple._2)
}
}
object FlinkWriteToMySQL {
def main(args: Array[String]): Unit = {
val namePrefix = "luyifan"
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 添加学生数据
val studentData = Seq(
s"100,${namePrefix}100",
s"101,${namePrefix}101",
s"102,${namePrefix}102_Math",
s"103,${namePrefix}103_CS",
s"104,${namePrefix}104_Physics"
)
val dataStream = env.fromCollection(studentData)
val tupleStream = dataStream.map { line =>
val parts = line.split(",", 2)
(parts(0).trim.toInt, parts(1).trim)
}
// 使用可序列化的语句构建器
val statementBuilder = new SerializableStatementBuilder
tupleStream.addSink(
JdbcSink.sink[(Int, String)](
"INSERT INTO student (Id, name) VALUES (?, ?) " +
"ON DUPLICATE KEY UPDATE name = VALUES(name)",
statementBuilder, // 使用显式的可序列化类
new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()
.withUrl("jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC")
.withDriverName("com.mysql.cj.jdbc.Driver")
.withUsername("root")
.withPassword("12345678")
.withConnectionCheckTimeoutSeconds(5)
.build
)
)
println(s"开始写入 ${studentData.size} 条学生数据到MySQL...")
env.execute("Flink Write to MySQL - luyifan")
}
}
```
### 关键修改说明:
1. **可序列化的语句构建器**:
```scala
class SerializableStatementBuilder extends JdbcStatementBuilder[(Int, String)] with Serializable {
override def accept(ps: PreparedStatement, tuple: (Int, String)): Unit = {
ps.setInt(1, tuple._1)
ps.setString(2, tuple._2)
}
}
```
- 显式实现 `Serializable` 接口
- 替代不可序列化的 lambda 表达式
- 保持与原始逻辑相同的功能
2. **JDBC Sink 使用方式**:
```scala
val statementBuilder = new SerializableStatementBuilder
tupleStream.addSink(JdbcSink.sink(..., statementBuilder, ...))
```
- 创建可序列化构建器的实例
- 避免使用匿名内部类或 lambda
### 额外优化建议:
#### 1. 解决 SLF4J 绑定冲突:
在 `src/main/resources` 添加 `log4j.properties` 文件:
```properties
log4j.rootLogger=ERROR, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
# 禁用无关日志
log4j.logger.org.apache.flink=ERROR
log4j.logger.org.apache.zookeeper=ERROR
log4j.logger.org.apache.hadoop=ERROR
log4j.logger.org.apache.kafka=ERROR
```
#### 2. Maven 依赖管理:
确保 `pom.xml` 中只有一个 SLF4J 绑定:
```xml
<dependencies>
<!-- 移除冲突的绑定 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.36</version>
<scope>provided</scope> <!-- 或完全移除 -->
</dependency>
<!-- 保留需要的绑定 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
```
### 原理解释:
1. **Flink 序列化要求**:
- 分布式执行需要序列化任务图
- Lambda 表达式使用合成类(synthetic classes)可能导致序列化问题
- 显式实现 `Serializable` 保证稳定序列化
2. **日志冲突根源**:
```plaintext
SLF4J: Found binding in [slf4j-log4j12.jar]
SLF4J: Found binding in [slf4j-simple.jar]
```
- 多个 SLF4J 实现在类路径中冲突
- 需要明确指定单一日志实现
3. **Flink 最佳实践**:
- 避免在算子中使用匿名内部类
- 为函数显式声明序列化版本 UID
- 保持函数无状态(减少序列化负担)