数据库下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()
  }
}
相关推荐
_codemonster5 小时前
30分钟快速搭建 Spring Cloud Alibaba 微服务实战(一)
微服务·架构·毕业设计·课程设计
Cosolar6 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
qcx238 小时前
【系统学AI】09 Multi-Agent架构(2026版):从学术理论到工业级实践
java·人工智能·架构·multi-agent·claude agent
wb043072018 小时前
厨房质检员——从阿明的“祖传配方“到标准化质检,看测试金字塔的落地
架构·log4j
Dongwoo Jeong8 小时前
微服务架构(MSA)是如何诞生的?
微服务·云原生·架构
张忠琳9 小时前
【kubernetes v1.21】(kubelet 1)Kubelet 核心架构与启动流程
云原生·架构·kubernetes·kubelet
用户9874092388710 小时前
超算中心 高性能计算 htc命令module use的作用
架构
AI科技星10 小时前
基于**v=c(空间光速螺旋运动)唯一第一性原理**重新完整求导证明
人工智能·线性代数·算法·机器学习·架构·概率论·学习方法
__log10 小时前
如何优雅地“借鉴”任何网站的设计系统
人工智能·架构·知识图谱
她的男孩12 小时前
从自然语言到数据大屏:Forge Report Studio 的 AI 生成链路
人工智能·后端·架构