Flink 系列第12篇:Flink 维表关联详解

一、需求场景

Flink 流式计算中,常需以原始数据流为基础,关联外部维表补充属性,满足业务分析需求。

示例:订单数据流中仅包含收货人所在省的 ID,需关联外部维表(存储省 ID 与省名称的映射关系),补充省名称属性,完善订单数据维度。

实际业务中,维表数据通常存储在 MySQL、HBase、Redis 等外部系统,且存在定时更新需求,需根据业务时效性要求选择合适的关联方案。

常用维表关联方案主要分为以下4类,基本覆盖所有业务场景:

  • 实时查询维表

  • 预加载全量数据

  • LRU 缓存

  • 其他衍生方案

二、实时查询维表

2.1 核心原理

实时查询维表是指在 Flink 算子中直接访问外部数据库(如 MySQL),采用同步调用方式,确保获取的维表数据是最新的。核心逻辑是在 Flink 算子(如 Map 算子)中建立外部系统连接,每条数据流触发一次外部查询,完成关联。

2.2 优缺点

  • 优点:数据实时性最高,确保关联的维表数据是最新状态;

  • 缺点:

    • 高并发场景下,会对外部系统造成巨大访问压力,QPS 需求极高(峰值可达十万到几十万),作业瓶颈转移至外部系统;

    • 同步调用易导致线程阻塞、Task 等待数据返回,影响整体任务吞吐量;

    • 外部系统异常(如连接失败、线程池满)会直接影响 Flink 任务稳定性。

2.3 代码实现(订单关联城市名称)

以订单数据为例,根据下单用户的城市 ID,关联 MySQL 维表获取城市名称,核心代码如下:

2.3.1 订单实体类

java 复制代码
public class Order {
    private Integer cityId;
    private String userName;
    private String items;
    private String cityName; // 关联维表后补充的属性

    // 无参构造
    public Order() {}

    // 带参构造(关联后使用)
    public Order(Integer cityId, String userName, String items, String cityName) {
        this.cityId = cityId;
        this.userName = userName;
        this.items = items;
        this.cityName = cityName;
    }

    // getter/setter 方法
    public Integer getCityId() {
        return cityId;
    }

    public void setCityId(Integer cityId) {
        this.cityId = cityId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getItems() {
        return items;
    }

    public void setItems(String items) {
        this.items = items;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    @Override
    public String toString() {
        return "Order{" +
                "cityId=" + cityId +
                ", userName='" + userName + '\'' +
                ", items='" + items + '\'' +
                ", cityName='" + cityName + '\'' +
                '}';
    }
}

2.3.2 实时查询维表算子

java 复制代码
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class DimSync extends RichMapFunction<String, Order> {

    private static final Logger LOGGER = LoggerFactory.getLogger(DimSync.class);
    private Connection conn = null; // 外部数据库连接

    /**
     * 初始化方法:Task启动时执行,创建数据库连接
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        // 建立MySQL连接(需引入MySQL驱动依赖)
        conn = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/dim?characterEncoding=UTF-8",
                "admin",
                "admin"
        );
    }

    /**
     * 核心处理方法:每条数据触发一次维表查询
     */
    @Override
    public Order map(String in) throws Exception {
        // 解析输入的JSON格式订单数据
        JSONObject jsonObject = JSONObject.parseObject(in);
        Integer cityId = jsonObject.getInteger("city_id");
        String userName = jsonObject.getString("user_name");
        String items = jsonObject.getString("items");

        // 根据city_id查询MySQL维表,获取city_name
        PreparedStatement pst = conn.prepareStatement("select city_name from info where city_id = ?");
        pst.setInt(1, cityId);
        ResultSet resultSet = pst.executeQuery();
        String cityName = null;
        while (resultSet.next()){
            cityName = resultSet.getString(1);
        }
        pst.close(); // 关闭PreparedStatement,避免资源泄露

        // 返回关联后的订单数据
        return new Order(cityId, userName, items, cityName);
    }

    /**
     * 关闭方法:Task结束时执行,释放数据库连接
     */
    @Override
    public void close() throws Exception {
        super.close();
        if (conn != null && !conn.isClosed()) {
            conn.close(); // 释放连接,避免打满MySQL连接数
        }
    }
}

2.4 注意事项

  • 适用场景:仅适用于小数据量维表查询,避免高并发场景下压垮外部系统;

  • 连接管理:需妥善处理外部系统连接,建议使用线程池管理连接,避免单条数据创建一个连接;

  • 资源释放:必须在 close\(\) 方法中释放数据库连接、PreparedStatement 等资源,防止连接泄露导致外部系统连接数耗尽,进而引发任务失败。

三、预加载全量数据

3.1 核心原理

为解决实时查询维表带来的外部系统频繁访问、连接压力大的问题,预加载全量数据采用"一次性加载+定时更新"的思路:

  • Flink 任务启动时,将外部维表的全量数据加载到内存中;

  • 数据流处理时,直接在内存中进行关联,无需访问外部数据库;

  • 通过定时任务拉取维表最新数据,更新内存中的缓存,解决维表数据更新无法感知的问题。

3.2 优缺点

  • 优点:仅需一次性访问外部系统,大幅降低外部系统压力,关联效率极高(内存查询);

  • 缺点:

    • 内存消耗大:全量维表数据缓存于内存,不适用于数据量较大的维表;

    • 数据延迟:定时更新机制导致维表数据存在延迟,无法实时获取最新数据;

    • 维表更新感知不及时:若维表数据更新频繁,定时更新间隔难以平衡延迟与性能。

3.3 代码实现(定时拉取城市维表)

java 复制代码
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class WholeLoad extends RichMapFunction<String, Order> {

    private static final Logger LOGGER = LoggerFactory.getLogger(WholeLoad.class);
    private ScheduledExecutorService executor; // 定时任务线程池
    private Map<String, String> cache; // 内存缓存,存储city_id与city_name的映射

    /**
     * 初始化方法:启动定时任务,定时拉取维表数据
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        // 初始化缓存(可使用HashMap、ConcurrentHashMap等线程安全集合)
        cache = new ConcurrentHashMap<>();
        // 初始化定时任务线程池,每隔5分钟拉取一次维表数据
        executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(
                this::load, // 定时执行的方法(拉取维表数据)
                5, // 延迟5秒后开始第一次执行
                5, // 每次执行间隔5分钟
                TimeUnit.MINUTES
        );
        // 首次启动时主动拉取一次数据
        load();
    }

    /**
     * 核心处理方法:从内存缓存中查询维表数据,完成关联
     */
    @Override
    public Order map(String value) throws Exception {
        JSONObject jsonObject = JSONObject.parseObject(value);
        Integer cityId = jsonObject.getInteger("city_id");
        String userName = jsonObject.getString("user_name");
        String items = jsonObject.getString("items");
        // 从内存缓存中获取city_name(无需访问外部数据库)
        String cityName = cache.get(String.valueOf(cityId));
        return new Order(cityId, userName, items, cityName);
    }

    /**
     * 维表数据拉取方法:从MySQL拉取全量维表数据,更新内存缓存
     */
    public void load() throws Exception {
        Class.forName("com.mysql.jdbc.Driver"); // 加载MySQL驱动
        Connection con = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/dim?characterEncoding=UTF-8",
                "admin",
                "admin"
        );
        PreparedStatement statement = con.prepareStatement("select city_id,city_name from info");
        ResultSet rs = statement.executeQuery();
        // 清空原有缓存,更新为最新数据(避免数据冗余)
        cache.clear();
        while (rs.next()) {
            String cityId = rs.getString("city_id");
            String cityName = rs.getString("city_name");
            cache.put(cityId, cityName);
        }
        // 释放资源
        rs.close();
        statement.close();
        con.close();
        LOGGER.info("维表数据拉取完成,缓存大小:{}", cache.size());
    }

    /**
     * 关闭方法:停止定时任务,释放资源
     */
    @Override
    public void close() throws Exception {
        super.close();
        if (executor != null && !executor.isShutdown()) {
            executor.shutdown(); // 停止定时任务线程池
        }
    }
}

3.4 注意事项

  • 适用场景:维表数据量小、实时性要求不高,且数据更新频率较低的场景;

  • 缓存选择:需使用线程安全的集合(如 ConcurrentHashMap),避免多线程操作缓存出现数据异常;

  • 定时间隔:根据维表更新频率设置合理的定时拉取间隔,平衡数据延迟与系统性能;

  • 资源释放:停止任务时需关闭定时任务线程池,避免线程泄露。

四、LRU 缓存

4.1 核心原理

LRU(Least Recently Used,最近最少使用)是一种缓存淘汰算法,核心逻辑是:当缓存容量达到上限时,淘汰最近最少使用的冷数据,保留查询频率高的热数据。

Flink 中使用 LRU 缓存关联维表的思路:

  • 利用 Flink 的 RichAsyncFunction(异步 IO)读取外部维表(如 HBase)数据到 LRU 缓存;

  • 关联时优先查询缓存,缓存命中则直接返回结果;

  • 缓存未命中时,通过异步客户端查询外部维表,将结果插入缓存后返回,同时触发 LRU 淘汰机制(若缓存满)。

补充:异步 IO(RichAsyncFunction)的核心作用是解决与外部系统交互时,网络延迟成为系统瓶颈的问题,通过并发异步请求,避免同步等待导致的线程阻塞,提升任务吞吐量。

4.2 优缺点

  • 优点:

    • 缓存热数据,减少外部系统访问次数,降低网络延迟和外部系统压力;

    • 异步 IO 支持并发请求,提升任务吞吐量;

    • 缓存容量可控,通过淘汰冷数据,降低内存消耗。

  • 缺点:

    • 数据存在延迟:缓存数据有过期时间,维表数据更新后无法实时同步;

    • 实现复杂度高于前两种方案,需配置缓存参数、异步客户端。

4.3 依赖与代码实现

4.3.1 依赖引入(HBase 异步客户端)

xml 复制代码
<!-- HBase 异步客户端依赖 -->
<dependency>
    <groupId>org.hbase</groupId>
    <artifactId>asynchbase</artifactId>
    <version>1.8.2</version>
</dependency>
<!-- Guava 缓存依赖(用于实现LRU缓存) -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>

4.3.2 LRU 缓存与异步关联代码

java 复制代码
import org.apache.flink.api.common.functions.RichAsyncFunction;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.util.Collector;
import org.hbase.async.GetRequest;
import org.hbase.async.HBaseClient;
import org.hbase.async.KeyValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSONObject;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class LRU extends RichAsyncFunction<String, Order> {

    private static final Logger LOGGER = LoggerFactory.getLogger(LRU.class);
    private static final String HBASE_TABLE = "info"; // HBase维表名称
    private Cache<String, String> lruCache; // LRU缓存
    private HBaseClient hbaseClient; // HBase异步客户端

    /**
     * 初始化方法:创建HBase客户端和LRU缓存
     */
    @Override
    public void open(Configuration parameters) throws Exception {
        super.open(parameters);
        // 1. 初始化HBase异步客户端(指定HBase地址和端口)
        hbaseClient = new HBaseClient("127.0.0.1", "7071");

        // 2. 初始化LRU缓存
        lruCache = CacheBuilder.newBuilder()
                .maximumSize(10000) // 缓存最大容量(10000条)
                .expireAfterWrite(60, TimeUnit.SECONDS) // 缓存过期时间(1分钟)
                .build(); // 默认使用LRU淘汰算法
    }

    /**
     * 异步处理方法:异步查询缓存/维表,完成关联
     */
    @Override
    public void asyncInvoke(String input, Collector<Order> resultFuture) throws Exception {
        // 解析输入的JSON格式订单数据
        JSONObject jsonObject = JSONObject.parseObject(input);
        Integer cityId = jsonObject.getInteger("city_id");
        String userName = jsonObject.getString("user_name");
        String items = jsonObject.getString("items");
        String cityIdStr = String.valueOf(cityId);

        // 1. 先查询LRU缓存
        String cacheCityName = lruCache.getIfPresent(cityIdStr);
        if (cacheCityName != null) {
            // 缓存命中,直接返回关联结果
            Order order = new Order();
            order.setCityId(cityId);
            order.setItems(items);
            order.setUserName(userName);
            order.setCityName(cacheCityName);
            resultFuture.collect(order);
            return;
        }

        // 2. 缓存未命中,异步查询HBase维表
        hbaseClient.get(new GetRequest(HBASE_TABLE, cityIdStr))
                .addCallback((List<KeyValue> arg) -> {
                    // 回调函数:HBase查询完成后执行
                    for (KeyValue kv : arg) {
                        String cityName = new String(kv.value());
                        // 构建关联后的订单数据
                        Order order = new Order();
                        order.setCityId(cityId);
                        order.setItems(items);
                        order.setUserName(userName);
                        order.setCityName(cityName);
                        // 发送结果
                        resultFuture.collect(order);
                        // 将查询结果存入LRU缓存,供后续使用
                        lruCache.put(cityIdStr, cityName);
                    }
                    return null;
                });
    }

    /**
     * 关闭方法:释放客户端和缓存资源
     */
    @Override
    public void close() throws Exception {
        super.close();
        if (hbaseClient != null) {
            hbaseClient.shutdown(); // 关闭HBase异步客户端
        }
        if (lruCache != null) {
            lruCache.invalidateAll(); // 清空缓存
        }
    }
}

4.4 注意事项

  • 异步 IO 使用:必须搭配异步客户端(如 HBase 异步客户端),若无异步客户端,可自行创建线程池模拟异步请求,避免同步等待;

  • 缓存配置:根据业务场景合理设置缓存容量和过期时间,平衡内存消耗与数据延迟;

  • 异常处理:需添加回调异常处理逻辑,避免 HBase 查询失败导致任务阻塞;

  • 适用场景:维表数据量大、存在明显冷热数据区分,且实时性要求中等的场景。

五、其他衍生方案

除上述三种核心方案外,可根据业务需求衍生出多种维表关联方式,主要包括:

  • 维表广播:将维表数据封装为广播流,与原始数据流进行广播连接,适用于维表数据量小、更新频率低的场景,可避免重复查询;

  • 自定义异步线程池:通过创建自定义线程池,模拟异步请求,解决无异步客户端的外部系统关联问题;

  • Flink SQL 扩展:扩展 Flink SQL 的维表关联功能,直接使用 SQL Join 语法关联外部维表(如 MySQL、HBase),简化开发流程,适用于 SQL 化开发场景。

六、方案选型建议

维表关联方案的选型需结合业务场景,核心关注三个维度:维表数据量、实时性要求、外部系统性能,具体建议如下:

  • 实时性要求极高、维表数据量小 → 实时查询维表;

  • 实时性要求低、维表数据量小 → 预加载全量数据;

  • 实时性要求中等、维表数据量大、存在冷热数据 → LRU 缓存(搭配异步 IO);

  • SQL 化开发、维表数据量适中 → Flink SQL 扩展维表关联;

  • 维表数据量小、更新频率低 → 维表广播。

总体而言,各类方案可相互结合衍生新的解决方案,核心是从业务本身出发,平衡数据实时性、系统性能和开发复杂度。

相关推荐
嘉立创FPC苗工2 小时前
多层 FPC 阻抗控制:从原理到实践的全流程指南
大数据·制造·fpc·电路板
查古穆2 小时前
AI Agent 开发的工业化道路:Harness 架构深度解析
大数据·人工智能
qyz_hr3 小时前
2026年AI招聘选购:5大品牌核心差异对比(红海云 / Moka / 北森 / 肯耐珂萨 / 金蝶)
大数据·人工智能
xcbrand3 小时前
工业制造品牌全案公司哪家专业
大数据·人工智能·python·制造
南棱笑笑生3 小时前
20260420给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Buildroot时使用ll命令
java·大数据·elasticsearch·rockchip
Irene19913 小时前
大数据开发中常见的排序算法
大数据·排序算法
蓝眸少年CY4 小时前
Azkaban - 入门教程
大数据·azkaban
清 晨4 小时前
海外社媒内容审核加强跨境卖家如何避免限流
大数据·人工智能·矩阵·新媒体运营·内容营销
Evand J4 小时前
【雷达跟踪代码介绍】基于matlab卡尔曼滤波器雷达多目标跟踪(双雷达 多目标 分布式融合)
分布式·matlab·目标跟踪·多目标跟踪·雷达跟踪