数据库下Lambda 架构(spark+flink)

大白话讲解架构逻辑:

先用 Flink 做实时数据摄入,对刚上传的药材图像和成分报告做初步的格式校验、特征提取和脏数据过滤,把处理好的数据实时写入 ClickHouse 做在线分析;再用 Spark 做 T+1 的离线批量处理,对全量数据做更复杂的质量核查、标签计算和关联挖掘,把加工好的宽表写入 Hive 做长期存储。另外我会在 Flink 和 Spark 之间加一层 Kafka 做数据缓冲,削平流量峰值,同时保证数据不丢失。

第一站:Kafka 缓冲------样品暂存区 你在质检中心门口设了一个大型货架区,所有送来的样品先放在货架上,贴上标签,按顺序排好。 这个货架的作用: 削峰填谷:就算一天来了10吨样品,货架都能暂时存下,不会把后面的工人压垮 保证不丢:样品放在货架上,不会莫名其妙消失 灵活取用:快通道的工人随时可以从货架拿样品处理,慢通道的工人晚上也可以从同一个货架取样品复查 这就是 Kafka 缓冲层------一个可靠的消息队列,上游只管往里放,下游按自己的节奏取。

第二站:Flink 实时处理------快通道 你安排了一组快速质检员(Flink),他们守在货架旁边,样品一上架就拿走处理,绝不拖延。 每个快速质检员做三件事: 步骤 做什么 打个比方 1. 格式校验 检查样品是否合格 照片糊不糊?报告单字迹清不清楚?药材标签有没有贴错?不合格的直接扔进"废品筐" 2. 特征提取 记录关键信息 拍张照片记录药材颜色、形状;把报告单上的成分含量抄下来 3. 脏数据过滤 筛掉明显异常 比如报告写"含量120%"这种不可能的数据,直接剔除 做完这三步,快速质检员会立刻把结果写在一张快查板上,供一线人员随时查看。 这块"快查板"就是 ClickHouse------一个专门做快速查询的数据库。

第三站:ClickHouse------快查板 这块板子有几个特点: 写上去就立刻能看到:实时质检员写完,大屏马上刷新 查询飞快:你想看"最近1小时哪个药材不合格最多",一秒钟就出结果 适合做统计:汇总、排序、求平均值都非常快

第四站:Spark 离线处理------慢通道 每天晚上,另一组深度质检员(Spark)开始工作。他们不抢快,而是踏踏实实地做细致活。 他们从货架上把当天所有样品(甚至更早的)都翻出来,做三件更深入的事: 步骤 做什么 打个比方 1. 质量核查 全量复核 不光看单份报告,还要比对历史记录------这个药材供应商最近10批货质量是稳定还是下滑? 2. 标签计算 打标签 综合各项指标给药材打标签:"优质""常规""待评估",方便以后筛选 3. 关联挖掘 找规律 把药材数据和采购记录、产地信息、气候数据关联起来------比如"云南产的当归质量普遍高于其他产地" 做完这些,他们会整理成一份正式档案,存入档案室。这个流程对吗 按照这个流程具体讲一下技术实现

详细技术:

第一站:Kafka 缓冲层技术实现

核心作用:解耦采集端与计算端,应对突发的上传峰值(例如每天下午5点集中上传)。

Topic 规划
python 复制代码
# 建议创建 3 个 Topic 实现数据分流
raw_herb_data:        # 原始数据主通道,3个分区,保留7天
  partitions: 6
  retention.ms: 604800000
  
herb_dirty_data:      # 脏数据死信队列,便于后续人工审核
  partitions: 2
  retention.ms: 2592000000  # 保留30天

herb_processed:       # 处理后的标准化数据(可选)
  partitions: 3
消息体设计 (JSON Schema)
python 复制代码
{
  "message_id": "uuid-xxxx-xxxx",
  "event_time": "2026-04-20T10:35:22.123Z",
  "source": "image_scanner_03",
  "data_type": "herb_inspection",
  "payload": {
    "batch_id": "BATCH20260420",
    "herb_name": "当归",
    "supplier_id": "SUP_0882",
    "images": [
      {
        "url": "https://oss.xxx.com/herb/img001.jpg",
        "type": "appearance"
      }
    ],
    "report": {
      "moisture": 12.5,
      "ash": 4.2,
      "active_ingredient": 0.85
    }
  }
}

这是整个架构的速度层,需要对每条数据做毫秒级响应。

python 复制代码
public class HerbQualityStreamJob {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        
        // 1. 配置 Checkpoint(保证 Exactly-Once 语义)
        env.enableCheckpointing(60000); // 每分钟做一次快照
        env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
        
        // 2. 消费 Kafka
        KafkaSource<String> source = KafkaSource.<String>builder()
            .setBootstrapServers("kafka:9092")
            .setTopics("raw_herb_data")
            .setGroupId("flink-herb-quality-group")
            .setStartingOffsets(OffsetsInitializer.latest())
            .setValueOnlyDeserializer(new SimpleStringSchema())
            .build();
        
        DataStream<String> rawStream = env.fromSource(source, 
            WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(10)), 
            "Kafka Source");
        
        // 3. 数据处理管线
        DataStream<HerbQualityRecord> processedStream = rawStream
            .map(new JsonParserMapFunction())           // JSON 解析
            .filter(new FormatValidationFilter())        // 格式校验
            .map(new FeatureExtractionMapFunction())     // 特征提取
            .filter(new DirtyDataFilter())               // 脏数据过滤
            .assignTimestampsAndWatermarks(...);         // 事件时间处理
        
        // 4. 分流:脏数据写入死信队列
        OutputTag<String> dirtyOutputTag = new OutputTag<String>("dirty-data"){};
        SingleOutputStreamOperator<HerbQualityRecord> mainStream = processedStream
            .process(new DirtyDataSplitFunction(dirtyOutputTag));
        
        // 脏数据单独输出到 Kafka
        mainStream.getSideOutput(dirtyOutputTag)
            .sinkTo(KafkaSink.<String>builder()
                .setBootstrapServers("kafka:9092")
                .setRecordSerializer(...)
                .build());
        
        // 5. 写入 ClickHouse
        mainStream.addSink(new ClickHouseSinkFunction());
        
        env.execute("Herb Quality Inspection Stream");
    }
}
python 复制代码
-- 实时表:采用 ReplacingMergeTree 支持数据更新
CREATE TABLE herb_quality_realtime (
    event_time      DateTime64(3),
    batch_id        String,
    herb_name       String,
    supplier_id     String,
    moisture        Float32,
    ash             Float32,
    active_ingredient Float32,
    image_features  Array(Float32),     -- 存储特征向量
    quality_flag    Enum8('pending'=0, 'qualified'=1, 'rejected'=2),
    process_time    DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree()
PARTITION BY toYYYYMMDD(event_time)
ORDER BY (herb_name, supplier_id, event_time);

-- 预聚合物化视图:1分钟窗口统计
CREATE MATERIALIZED VIEW herb_minute_stats
ENGINE = SummingMergeTree()
ORDER BY (window_time, herb_name)
AS SELECT
    toStartOfMinute(event_time) as window_time,
    herb_name,
    count() as sample_count,
    avg(moisture) as avg_moisture,
    countIf(quality_flag = 'rejected') as rejected_count
FROM herb_quality_realtime
GROUP BY window_time, herb_name;

第三站:Spark 离线处理层技术实现

这是 批处理层,负责全量数据的深度加工。

Spark Job 核心代码结构
python 复制代码
object HerbBatchAnalytics {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("HerbBatchAnalytics")
      .enableHiveSupport()
      .getOrCreate()
    
    // 1. 读取前一天的全量数据
    val yesterday = LocalDate.now().minusDays(1).toString
    val rawDF = spark.read
      .parquet(s"hdfs://datalake/raw/herb/dt=$yesterday")
    
    // 2. 质量核查:对比历史趋势
    val qualityCheckDF = rawDF
      .groupBy("supplier_id", "herb_name")
      .agg(
        avg("moisture").as("avg_moisture"),
        stddev("moisture").as("std_moisture"),
        count("*").as("batch_count")
      )
      .withColumn("quality_trend", 
        when(col("std_moisture") < 0.5, "stable").otherwise("volatile"))
    
    // 3. 标签计算:综合打分
    val labeledDF = rawDF.join(qualityCheckDF, Seq("supplier_id", "herb_name"))
      .withColumn("score", 
        // 评分公式:水分占比30% + 灰分占比20% + 有效成分占比50%
        lit(0.3) * (col("moisture") / lit(15.0)) +
        lit(0.2) * (lit(5.0) / col("ash")) +
        lit(0.5) * (col("active_ingredient") / lit(0.9))
      )
      .withColumn("label",
        when(col("score") > 0.85, "premium")
          .when(col("score") > 0.70, "standard")
          .otherwise("review"))
    
    // 4. 关联挖掘:与产地/气候数据关联
    val supplierDF = spark.table("dim_supplier")   // 维度表
    val weatherDF = spark.table("fact_weather")    // 气象事实表
    
    val enrichedDF = labeledDF
      .join(supplierDF, "supplier_id")
      .join(weatherDF, 
        labeledDF("harvest_date").between(weatherDF("date_start"), weatherDF("date_end")))
    
    // 5. 写入 Hive 分区表(Iceberg 格式更优)
    enrichedDF.write
      .mode(SaveMode.Overwrite)
      .partitionBy("dt")
      .format("iceberg")  // 或 parquet
      .saveAsTable("dwd_herb_quality_daily")
    
    spark.stop()
  }
}
相关推荐
禅思院2 小时前
从术到道:构建企业级异步组件加载方案的设计哲学与实现精要
前端·vue.js·架构
csgo打的菜又爱玩2 小时前
6.MetricService 启动流程解析
大数据·flink
lwf0061642 小时前
Architecture Diagram Generator + Excalidraw + 飞书绘制架构图操作指南
架构·飞书
tq10862 小时前
配置、CPS、插件与Lisp宏:规则延迟固化的四种能力层次
架构·lisp
byte轻骑兵2 小时前
【HID】规范精讲[2]: 蓝牙HID架构解密——从USB到无线的底层设计逻辑
架构·人机交互·蓝牙·hid·蓝牙键盘·蓝牙鼠标
无心水2 小时前
15、商业API巅峰对决|Adobe vs Google vs Azure 高精度OCR全方位评测
架构·pdf·pdf解析·pdf抽取·paddlercor
2301_780789662 小时前
CDN加速与流量管理的最佳结合
网络·安全·web安全·架构·ddos
China_Yanhy2 小时前
架构决策记录 (ADR):全链路追踪方案选型对比评估
架构
张忠琳2 小时前
【openclaw】OpenClaw Tasks 模块超深度架构分析
ai·架构·openclaw