4. 批处理基础:Spark SQL 与 DataSet API、批处理 ETL 流程、与实时计算的异同

在风控系统中,批处理承担着“历史回溯、模型训练、离线报表、批量名单加工”等核心任务。本章将深入讲解 Spark SQL 与 DataSet API 在批处理场景下的应用,梳理标准批处理 ETL 流程,并系统对比批处理与实时计算在架构、延迟、数据语义上的本质差异。

4.1 Spark SQL 与 DataSet API 核心概念

Spark SQL 是 Spark 生态中用于结构化数据处理的模块,它提供了 DataFrame 和 DataSet 两种高级抽象。在风控批处理中,我们主要使用 DataSet API 来获得编译时类型安全与强类型检查。

特性 DataFrame DataSet
类型安全 弱类型(Row 对象) 强类型(JVM 对象)
编译时检查 无 有
适用场景 快速探索、SQL 查询 复杂 ETL、业务逻辑封装
序列化方式 Spark 内部 Row 格式 Encoders(Kryo / Java)
风控推荐度 中等(适合临时分析) 高(适合生产级规则加工)
实践建议:在风控批处理作业中,优先使用 DataSet[RiskEvent] 或 DataSet[UserProfile] 等强类型结构,可以大幅减少运行时类型转换错误,并提升代码可维护性。

4.2 批处理 ETL 标准流程

一个典型的风控批处理 ETL 作业通常包含以下五个阶段。每个阶段都有明确的输入输出和异常处理策略。

  1. 数据源接入(Source):从 HDFS、Hive 表、Kafka(批量消费)或 JDBC 读取原始数据,如交易流水、用户注册日志。
  2. 数据清洗(Clean):过滤无效字段、处理空值、去重、格式标准化。例如将时间戳统一为 UTC+8。
  3. 特征加工(Transform):基于业务规则进行聚合、关联、窗口计算。例如计算用户过去 7 天交易总金额、设备指纹聚类。
  4. 规则匹配(Match):将加工后的特征与风控规则库(如决策树、名单库)进行匹配,输出风险等级与处置建议。
  5. 结果输出(Sink):将结果写入 Hive 分区表、MySQL 或 ES,供后续报表、模型训练或实时缓存使用。
ETL 流程关键指标:数据延迟(通常 T+1)、吞吐量(GB/小时)、数据质量(空值率 < 0.1%)、任务成功率(> 99.5%)。
批处理 ETL 标准流程 数据源接入 数据清洗 特征加工 规则匹配 结果输出 各阶段典型操作 • 数据源接入:HDFS 读取 / Hive 表扫描 / Kafka 批量拉取 • 数据清洗:空值填充 / 去重 / 时间戳标准化 / 字段类型转换 • 特征加工:groupBy + agg / window 函数 / 多表 join • 规则匹配:名单命中 / 阈值比较 / 决策树打分 • 结果输出:Hive 分区写入 / MySQL 更新 / ES 索引

4.3 批处理与实时计算的异同

在风控系统中,批处理和实时计算并非互斥,而是互补。下表从多个维度对比两者的核心差异,帮助你在架构选型时做出正确判断。

对比维度 批处理(Spark SQL / DataSet) 实时计算(Flink / Spark Streaming)
数据延迟 分钟级 ~ 小时级(T+1 常见) 毫秒级 ~ 秒级
处理模式 有界数据集(Bounded) 无界数据流(Unbounded)
触发方式 定时调度(Crontab / Airflow) 事件驱动 / 持续运行
计算语义 Exactly-Once(通过 Checkpoint + 幂等输出) Exactly-Once(通过状态后端 + 两阶段提交)
状态管理 无状态或外部状态(Redis / HBase) 内置状态后端(RocksDB / Heap)
典型风控场景 离线名单生成、模型训练、历史回溯、报表 实时交易拦截、设备指纹在线匹配、反欺诈实时决策
资源消耗 高吞吐、高内存(可弹性伸缩) 低延迟、持续占用资源
容错恢复 任务重跑(全量或增量) 基于 Checkpoint 的增量恢复
架构陷阱:不要试图用批处理引擎去处理毫秒级实时风控场景,也不要将实时计算引擎用于大规模历史回溯(成本高且无必要)。正确的做法是“批流分离,数据同源”,即同一份原始数据同时进入批处理和实时两条链路,最终在服务层统一风险视图。

4.4 批处理代码示例:DataSet API 实现 ETL

以下代码展示了一个典型的风控批处理 ETL 作业,使用 Spark DataSet API 进行强类型转换。

import org.apache.spark.sql.{SparkSession, Encoders}
import org.apache.spark.sql.functions._

// 定义强类型 case class
case class RawTransaction(
  transId: String,
  userId: String,
  amount: Double,
  transTime: String,
  deviceId: String
)

case class RiskFeature(
  userId: String,
  totalAmount7d: Double,
  transCount7d: Long,
  deviceCount7d: Long
)

val spark = SparkSession.builder()
  .appName("RiskBatchETL")
  .enableHiveSupport()
  .getOrCreate()

import spark.implicits._

// 1. 读取原始数据(Hive 表)
val rawDS: Dataset[RawTransaction] = spark
  .table("ods.transaction_log")
  .as[RawTransaction]

// 2. 数据清洗:过滤异常金额、标准化时间
val cleanedDS = rawDS
  .filter(_.amount > 0)
  .filter(_.transId.nonEmpty)
  .map(t => t.copy(transTime = normalizeTime(t.transTime)))

// 3. 特征加工:过去7天聚合
val riskFeatureDS: Dataset[RiskFeature] = cleanedDS
  .groupByKey(_.userId)
  .agg(
    sum("amount").as[Double],
    count("transId").as[Long],
    countDistinct("deviceId").as[Long]
  )
  .map { case (userId, totalAmt, cnt, devCnt) =>
    RiskFeature(userId, totalAmt, cnt, devCnt)
  }

// 4. 规则匹配(示例:金额超过阈值)
val highRiskDS = riskFeatureDS.filter(_.totalAmount7d > 100000)

// 5. 结果输出到 Hive 分区表
highRiskDS
  .toDF()
  .write
  .mode("overwrite")
  .format("parquet")
  .partitionBy("dt")
  .saveAsTable("dwd.risk_user_7d")
编码要点:使用 .as[T] 将 DataFrame 转换为 DataSet 时,需要隐式 Encoder(通过 import spark.implicits._ 或手动定义)。聚合操作后返回的是 TypedColumn,配合 groupByKey 可保持类型安全。

4.5 批流一体架构下的协同策略

在批流一体风控系统中,批处理与实时计算并非孤立运行,而是通过以下机制协同工作:

  • 数据同源:批处理和实时计算从同一 Kafka Topic 或 Hive 表读取原始数据,保证特征一致性。
  • 特征互补:实时计算产出秒级特征(如当前会话行为),批处理产出历史统计特征(如 30 天累计金额),两者在决策服务层融合。
  • 模型同步:批处理负责模型训练和离线评估,将模型参数(如阈值、权重)写入共享存储(Redis / MySQL),实时计算在运行时加载最新模型。
  • 结果校准:批处理每日对实时决策结果进行回溯校验,发现偏差后触发告警或自动修正规则。
批流一体风控协同架构 统一数据源 批处理(T+1) 实时计算(秒级) 共享存储(Redis / MySQL)
协同收益:批处理提供“全量、准确、可回溯”的历史视角,实时计算提供“低延迟、高响应”的在线能力。两者结合,风控系统既能拦截实时欺诈,又能通过离线分析持续优化规则,形成闭环。