Flink KeySelector

结论

  1. FLink KeyBy算子的KeySelector的构造是在JobManager/客户端构造的 ,而getKey获取key去进行分区,是在TaskManager的subTask中执行的,因此,这就涉及到一个数据传递的问题。
  2. Flink的并行度,也就是subTask其实底层就是一个个互相隔离的Task线程,不了解的可以先看Flink TM、subTask、JVM、算子链、slot、slot组的关系
  3. Session 模式下,多个任务共享集群资源,但 Flink 会通过类加载隔离(如 ChildFirstClassLoader)和任务执行上下文隔离,保证不同任务的用户代码(包括 JsonKeySelector2)相互独立
  4. 线程级隔离(subTask之间) :ThreadLocal每个线程生成独立的 Field 缓存副本。即使两个任务的 subtask 运行在同一个 TM 的不同线程中,它们的 usedFieldCache 也会各自持有 "属于当前线程" 的缓存,不会互相覆盖或干扰
  5. 任务级隔离(同一session,不同FlinkJob间) :Flink 也会通过以下机制保证隔离,即使两个任务的 subtask 运行在同一个 TM 的相同线程中(极端情况,比如资源超配时的线程复用)
    • 类加载隔离:每个任务的用户代码(包括 JsonKeySelector)由独立的类加载器加载,避免类定义 / 静态变量串用;
    • 执行上下文隔离:任务的状态、算子逻辑的初始化参数(如 jsonKeyFields)都是任务专属的,不同任务的 JsonKeySelector 实例在初始化时,会根据自身任务的配置加载字段,与其他任务无关。
  6. Field本身可以序列化,但是Field一旦关联上某个字段,并获取了这个字段的上下文信息后,是不可序列化和反序列化的,会报错java.io.NotSerializableException: java.lang.reflect.Field ,而transient修饰会让其不参与序列化
  7. readObject()是 Java自定义反序列化机制 的核心方法,与其相对的是序列化机制writeObject(),他有具体被调用的规则如下
    1. 当前类实现了java.io.Serializable接口
    2. 方法必须被定义为private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

1.自定义JsonKeySelector

功能:通过传入一个keyFiedls数组,去实现分组操作

java 复制代码
public class JsonKeySelector<IN> implements KeySelector<IN, Long>, Serializable {
    private static final Logger LOGGER = LoggerFactory.getLogger(JsonKeySelector2.class);
    private static final long serialVersionUID = 1L;
    private static final char SPLIT_CHAR = '\001';
    private static final char NULL_MARKER = '\002';

    // 核心配置变量
    private final List<String> jsonKeyFields; // 配置的JSON分组字段
    private final boolean isAutoSalt; // 加盐开关
    private final String autoSalt; // 盐值
    private final Class<IN> bindPojoClass; // POJO类

    /**  
    * 1.Field不可序列化和反序列化,因此需要用transient修饰  
    * 2.采用ThreadLocal是确保线程安全,Flink多并行度底层其实也是多线程,只不过用线程隔离机制隔开了  
    */
    private transient ThreadLocal<Map<String, Field>> usedFieldCache;

    /**
     * 构造函数,在JobManager/client中被调用
     */
    public JsonKeySelector2(List<String> jsonKeyFields, boolean isAutoSalt, String pojoName) {
        // 参数校验
        if (jsonKeyFields == null || jsonKeyFields.isEmpty()) {
            throw new IllegalArgumentException("JSON Key Fields name is null");
        }
        if (StringUtils.isEmpty(pojoName)) {
            throw new IllegalArgumentException("POJO name is null");
        }

        // 加载POJO类
        Class<IN> pojoClass;
        try {
            pojoClass = (Class<IN>) Class.forName(pojoName);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("POJO {} is not exists :" + pojoName, e);
        }

        // 初始化配置
        this.jsonKeyFields = jsonKeyFields;
        this.isAutoSalt = isAutoSalt;
        this.autoSalt = isAutoSalt ? UUID.randomUUID().toString() : "";
        this.bindPojoClass = pojoClass;

        // 初始化Field缓存 -- 其实用的反射去获取jsonKeyFields的Fields进行缓存
        initFieldCache();

        LOGGER.info("Initialization completed, POJO: {}, KeyBy fields: {}", pojoClass.getSimpleName(), jsonKeyFields);
    }

    /**
     * 优化核心:用映射表替代嵌套循环,时间复杂度从O(n*m)降为O(n+m)
     */
    private void initFieldCache() {
        this.usedFieldCache = ThreadLocal.withInitial(() -> {
            // 1. 构建POJO字段的"JSON键→Field"映射表(只遍历一次POJO字段)
            Map<String, Field> pojoJsonFieldMap = new HashMap<>();
            Field[] allPojoFields = bindPojoClass.getDeclaredFields();
            for (Field pojoField : allPojoFields) {
                JsonProperty jsonProp = pojoField.getAnnotation(JsonProperty.class);
                if (jsonProp != null) {
                    String jsonKey = jsonProp.value();
                    // 若有重复的@JsonProperty值,后面的会覆盖前面的(符合注解使用规范)
                    pojoJsonFieldMap.put(jsonKey, pojoField);
                }
            }

            // 2. 遍历配置的JSON字段,从映射表中O(1)查找(无嵌套循环)
            Map<String, Field> fieldCache = new HashMap<>(jsonKeyFields.size());
            for (String targetJsonKey : jsonKeyFields) {
                Field matchedField = pojoJsonFieldMap.get(targetJsonKey);
                if (matchedField == null) {
                    throw new RuntimeException("No field in POJO is annotated with @JsonProperty(\"" + targetJsonKey + "\")");
                }
                // 开启私有字段访问权限
                matchedField.setAccessible(true);
                fieldCache.put(targetJsonKey, matchedField);
            }

            LOGGER.info("FieldCache initialization done, number of fields loaded: {}", fieldCache.size());
            return fieldCache;
        });
    }

    /**
     * Flink TaskManager的subTask获取JobManager发来的信息,会进行反序列化,此时我们进行重建缓存
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 保留默认反序列化逻辑
        in.defaultReadObject();
        // 对transient修饰的属性将进行重新初始化
        initFieldCache();
    }

    @Override
    public Long getKey(IN record) throws Exception {
        Map<String, Field> fieldCache = usedFieldCache.get();
        Hasher hasher = Hashing.murmur3_128().newHasher();
        for (String jsonKey : jsonKeyFields) {
            Field field = fieldCache.get(jsonKey);
            Object value = field.get(record);
            if (value == null) {
                hasher.putChar(NULL_MARKER);
            } else {
                putValue(hasher, value);
            }
            hasher.putChar(SPLIT_CHAR);
        }

        if (isAutoSalt) {
            hasher.putString(autoSalt, StandardCharsets.UTF_8);
        }

        Long key = hasher.hash().padToLong();
        return key;
    }

    /** 按类型写哈希 */
    private void putValue(Hasher hasher, Object value) {
        if (value instanceof Long) hasher.putLong((Long) value);
        else if (value instanceof Integer) hasher.putInt((Integer) value);
        else if (value instanceof Boolean) hasher.putBoolean((Boolean) value);
        else if(value instanceof Double) hasher.putDouble((Double) value);
        else hasher.putString(value.toString(), StandardCharsets.UTF_8);
    }

2.现象

Flink的JobManager的log如下 Flink的TaskManager的log如下 从日志打印,我们可以看出

  1. FieldCache initialization done, number of fields loaded: {}日志只在TaskManager中打印,它属于initFieldCache(),而构造函数和readObject()都有调用它,但构造函数会多打印Initialization completed, POJO: {}, KeyBy fields: {},但是TaskManager中并没有这个日志,说明他是readObject()进行反序列化的时候调用的,因此,反序列化和getKey的操作是由TaskManager的subTask执行的,且根据并行度会打印对应数量的日志
  2. Initialization completed, POJO: {}, KeyBy fields: {}这个日志在JobManager中打印,说明构造函数是在客户端中调用的,客户端执行后会生成JobGraph然后传给JobManager,日志也在JobManager中
  3. 构造函数中也调用了initFieldCache()但是为啥没打印FieldCache initialization done, number of fields loaded: {}呢?原因在于:ThreadLocal.withInitial()是懒加载,他只是先创建这么个实例,但是真正执行里面的Lambda表达式是在首次调用usedFieldCache.get() 时才会执行 ,而构造函数中并没有调usedFieldCache.get()所以在JobManager的日志中根本没有FieldCache initialization done, number of fields loaded: {}这个日志
相关推荐
两点王爷11 小时前
PostgreSQL 常用 SQL 语句与 GIS 相关函数详解
数据库·后端
考虑考虑12 小时前
Springboot环境变量占位符语法
spring boot·后端·spring
RoboWizard13 小时前
三星和金士顿内存条哪个更适合游戏超频
大数据·人工智能
深圳市恒星物联科技有限公司13 小时前
轻量MCU设备通过OpenHarmony兼容性测评的全流程关键要点与实战踩坑经验
大数据·人工智能·物联网·鸿蒙
TDengine (老段)13 小时前
TDengine 常见问题 TOP3
大数据·数据库·物联网·时序数据库·tdengine·涛思数据
步行cgn14 小时前
Spring 基于 XML 的自动装配:byName 详解
java·后端·spring
Leo.yuan15 小时前
盘点2026十大主流国产数据仓库软件:从开源到商业,企业级怎么选?
大数据
长谷深风11115 小时前
评测AI Agent:三种裁判各司其职
java·大数据·开发语言·人工智能·ai agent
RISCV_Explorer16 小时前
RISC-V启动与运行时规范机制解析——BRS约定、HSM多核启动与设备树要求
后端·risc-v
星栈独行16 小时前
ADK-Rust 是什么?Rust 生态新一代 AI Agent 开发套件
人工智能·后端·rust