Flink Task 执行源码深度剖析:从 Task 线程启动到算子链执行的完整链路

前面讲了 Flink Task 调度任务的完整链路(创建 Execution → 调度 → 申请 Slot → 部署 Task → 运行执行 → 完成或失败),这篇聚焦 Flink Task 执行的源码深度剖析。Task 部署到 TaskManager 后,Task 线程如何启动?OperatorChain 如何创建?算子的 open()/processElement()/close() 如何调用?数据如何在算子间传递?RecordWriter 和 RecordReader 如何工作?反压机制如何实现?这篇从源码级别跟踪 Flink Task 执行的完整链路。


一、Task 执行整体架构

下面这张图是 Task 执行整体架构,包括 TaskManager 内部组件、内存与环境管理、外部交互和执行流程。

Flink Task 执行涉及三个核心部分:

  • TaskManager(工作节点):运行 TaskExecutor、Task、OperatorChain、StreamTask、RecordWriter、RecordReader、InputGate、ResultPartition。TaskExecutor 接收 submitTask 请求,创建 Task 实例并启动执行;Task 是具体的执行单元,实现 Runnable 接口;OperatorChain 串联多个算子,数据在算子间通过内存直接传递;RecordWriter 输出数据到 ResultPartition,RecordReader 从 InputGate 读取上游数据。
  • 内存与环境管理:运行 MemoryManager(内存管理器,管理堆内堆外内存)、NetworkEnvironment(网络环境,管理网络缓冲和连接)、StateBackend(状态后端,管理状态存储和恢复)、IOManager(IO 管理器,管理磁盘 IO)、ShuffleEnvironment(Shuffle 环境,管理数据 Shuffle 传输)。这些组件为 Task 执行提供内存、网络、状态、IO 等基础设施。
  • 外部交互:Task 执行过程中与 JobMaster(提交 Task,接收状态更新)、上游 TaskManager(通过网络发送数据)、下游 TaskManager(通过网络接收数据)、Source 外部系统(Kafka/HDFS/MySQL 等数据源)、Sink 外部系统(Kafka/ES/MySQL 等数据汇)、Checkpoint 存储(HDFS/S3 等持久化存储)交互。

Task 执行核心流程5步:启动 Task 线程 → 创建 OperatorChain → open 算子 → processElement 处理数据 → close 释放资源。


二、Task 执行完整流程

下面这张图是 Task 执行完整流程,包括6步执行流程、方法调用链、核心类和常见问题。

Task 执行分为6步:

第1步:启动 Task 线程。TaskExecutor 收到 JobMaster 的 submitTask 请求后,首先验证作业 ID 和 Slot 是否有效,然后创建 Task 实例,启动 Task 线程。Task 实现 Runnable 接口,run() 方法是核心执行逻辑。Task 线程启动后,状态从 DEPLOYING 转为 RUNNING,通知 JobMaster Task 已启动。

第2步:创建 OperatorChain。Task 线程启动后,创建 OperatorChain(算子链)。OperatorChain 包含链内所有算子,创建 RecordWriter(输出)和 RecordReader(输入),初始化 InputGate 和 ResultPartition。OperatorChain 将多个算子串联,数据在算子间通过内存直接传递,减少序列化和网络开销。算子链优化是 Flink 性能优化的重要手段,将可以串联的算子(如 map→filter→map)合并到一个 Task 中执行。

第3步:调用 open() 初始化算子。OperatorChain 创建完成后,依次调用链内所有算子的 open() 方法。open() 方法用于初始化算子状态,如创建连接池、初始化状态后端、加载广播变量、注册定时器等。open() 方法在处理数据之前调用,确保算子准备就绪。RichFunction 接口提供 open() 和 close() 生命周期方法,普通 Function 没有生命周期方法。

第4步:processElement() 处理数据。算子初始化完成后,StreamTask 进入数据处理循环。RecordReader 从 InputGate 读取上游数据,反序列化为记录,调用第一个算子的 processElement() 方法。数据在算子链内依次传递,每个算子处理后输出到下一个算子。链尾算子通过 RecordWriter 将数据写入 ResultPartition,通过网络发送到下游。处理循环持续直到所有数据处理完成或任务被取消。

第5步:调用 close() 释放资源。所有数据处理完成后,依次调用链内所有算子的 close() 方法。close() 方法用于释放算子资源,如关闭连接池、刷新状态、清理定时器、关闭文件句柄等。close() 方法在处理数据之后调用,确保资源正确释放。close() 方法调用完成后,OperatorChain 释放 RecordWriter 和 RecordReader,关闭 InputGate 和 ResultPartition。

第6步:完成释放,通知 JobMaster。Task 执行完成后,状态从 RUNNING 转为 FINISHED,释放分配的 Slot,通知 JobMaster Task 已完成。JobMaster 收到完成通知后,标记对应 Execution 为 FINISHED,通知下游 Execution 可以调度。如果 Task 执行失败,状态转为 FAILED,触发故障恢复策略,重新调度受影响的 Task。


三、核心类源码剖析

3.1 Task 任务实例

java 复制代码
public class Task implements Runnable {
    private final JobID jobId;
    private final JobInformation jobInformation;
    private final TaskInformation taskInformation;
    private final ExecutionAttemptID executionId;
    private final AllocationID allocationId;
    private volatile Thread executingThread;
    private volatile ExecutionState executionState;

    public void startTaskThread() {
        executingThread = new Thread(this, "Task Thread");
        executingThread.start();
    }

    @Override
    public void run() {
        try {
            // 1. 状态转换:DEPLOYING → RUNNING
            executionState = ExecutionState.RUNNING;
            notifyJobManager(ExecutionState.RUNNING);

            // 2. 创建算子链 OperatorChain
            OperatorChain operatorChain = new OperatorChain(
                taskInformation,
                jobInformation,
                executionId,
                memoryManager,
                ioManager,
                networkEnvironment,
                stateBackend,
                checkpointStorage,
                checkpointResponder,
                kvStateService,
                shuffleEnvironment);

            // 3. 调用所有算子的 open() 方法
            operatorChain.openOperators();

            // 4. 处理数据,调用 processElement()
            operatorChain.processInputs();

            // 5. 调用所有算子的 close() 方法
            operatorChain.closeOperators();

            // 6. 状态转换:RUNNING → FINISHED
            executionState = ExecutionState.FINISHED;
            notifyJobManager(ExecutionState.FINISHED);

        } catch (Throwable t) {
            // 处理异常,状态转换:RUNNING → FAILED
            executionState = ExecutionState.FAILED;
            notifyJobManager(ExecutionState.FAILED, t);
        } finally {
            // 释放资源
            cleanup();
        }
    }

    public void cancelExecution() {
        if (executionState == ExecutionState.RUNNING) {
            executionState = ExecutionState.CANCELING;
            executingThread.interrupt();
        }
    }

    private void notifyJobManager(ExecutionState state, Throwable... cause) {
        TaskExecutionState taskState = new TaskExecutionState(executionId, state, cause);
        taskManagerActions.updateTaskExecutionState(taskState);
    }

    private void cleanup() {
        // 释放 Slot,关闭资源
        releaseSlot();
        closeResources();
    }
}

Task 是具体的任务执行单元,实现 Runnable 接口。run() 方法是核心执行逻辑:首先将状态从 DEPLOYING 转为 RUNNING,通知 JobMaster;然后创建 OperatorChain 算子链;接着调用所有算子的 open() 方法初始化;然后处理数据,调用 processElement();处理完成后调用 close() 方法释放资源;最后将状态转为 FINISHED,通知 JobMaster。如果执行过程中出现异常,catch 块捕获异常,状态转为 FAILED,通知 JobMaster 触发故障恢复。cancelExecution() 方法取消正在执行的 Task,通过 interrupt() 中断 Task 线程。

3.2 TaskExecutor 任务执行器

java 复制代码
public class TaskExecutor implements TaskExecutorGateway {
    private final Map<ExecutionAttemptID, Task> tasks;
    private final SlotTable slotTable;
    private final MemoryManager memoryManager;

    @Override
    public CompletableFuture<Acknowledge> submitTask(
            TaskDeploymentDescriptor tdd,
            JobMasterId jobMasterId,
            Time timeout) {
        // 1. 验证作业 ID 和 Slot
        JobID jobId = tdd.getJobId();
        AllocationID allocationId = tdd.getAllocationId();
        if (!slotTable.tryMarkSlotActive(jobId, allocationId)) {
            throw new TaskSubmissionException("Slot not found or not active");
        }

        // 2. 创建 Task 实例
        Task task = new Task(
            jobId,
            tdd.getJobInformation(),
            tdd.getTaskInformation(),
            executionAttemptId,
            allocationId,
            taskManagerActions,
            inputSplitProvider,
            checkpointResponder,
            memoryManager,
            ioManager,
            networkEnvironment,
            libraryCacheManager,
            fileCache,
            shuffleEnvironment,
            kvStateService,
            broadcastVariableManager,
            taskEventDispatcher,
            externalResourceInfoProvider,
            fatalErrorHandler);

        // 3. 启动 Task 线程
        tasks.put(executionAttemptId, task);
        task.startTaskThread();

        return CompletableFuture.completedFuture(Acknowledge.get());
    }

    @Override
    public void updateTaskExecutionState(TaskExecutionState taskExecutionState) {
        ExecutionAttemptID executionId = taskExecutionState.getID();
        Task task = tasks.get(executionId);
        if (task != null) {
            task.notifyUpdate(taskExecutionState);
        }
    }

    @Override
    public CompletableFuture<Acknowledge> cancelTask(
            ExecutionAttemptID executionAttemptId,
            Time timeout) {
        Task task = tasks.get(executionAttemptId);
        if (task != null) {
            task.cancelExecution();
        }
        return CompletableFuture.completedFuture(Acknowledge.get());
    }
}

TaskExecutor 是 Task 执行器,运行在 TaskManager 上。submitTask() 方法接收 JobMaster 的 submitTask 请求,首先验证作业 ID 和 Slot 是否有效,然后创建 Task 实例,启动 Task 线程执行。updateTaskExecutionState() 方法接收 Task 状态更新,通知 JobMaster。cancelTask() 方法取消正在执行的 Task。TaskExecutor 管理所有正在运行的 Task,负责 Task 的生命周期管理。

3.3 OperatorChain 算子链

java 复制代码
public class OperatorChain {
    private final StreamOperator<?>[] allOperators;
    private final RecordWriter<?>[] recordWriters;
    private final RecordReader<?>[] recordReaders;
    private final InputGate[] inputGates;
    private final ResultPartition[] resultPartitions;

    public OperatorChain(...) {
        // 1. 创建 InputGate 和 ResultPartition
        this.inputGates = createInputGates();
        this.resultPartitions = createResultPartitions();

        // 2. 创建 RecordReader 和 RecordWriter
        this.recordReaders = createRecordReaders(inputGates);
        this.recordWriters = createRecordWriters(resultPartitions);

        // 3. 创建算子实例(从链尾到链头)
        this.allOperators = createOperators();
    }

    public void openOperators() throws Exception {
        // 依次调用所有算子的 open() 方法
        for (StreamOperator<?> operator : allOperators) {
            operator.open();
        }
    }

    public void processInputs() throws Exception {
        // 数据处理循环
        while (isRunning) {
            // 从 RecordReader 读取下一条数据
            StreamRecord record = recordReaders[0].getNext();
            if (record == null) {
                break; // 所有数据处理完成
            }

            // 调用第一个算子的 processElement()
            // 数据在算子链内依次传递
            allOperators[0].processElement(record);
        }
    }

    public void closeOperators() throws Exception {
        // 依次调用所有算子的 close() 方法
        for (StreamOperator<?> operator : allOperators) {
            operator.close();
        }
    }

    public void releaseResources() {
        // 释放 RecordWriter 和 RecordReader
        for (RecordWriter<?> writer : recordWriters) {
            writer.close();
        }
        for (RecordReader<?> reader : recordReaders) {
            reader.close();
        }
        // 关闭 InputGate 和 ResultPartition
        for (InputGate gate : inputGates) {
            gate.close();
        }
        for (ResultPartition partition : resultPartitions) {
            partition.close();
        }
    }
}

OperatorChain 是算子链,将多个算子串联到一个 Task 中执行。构造方法创建 InputGate 和 ResultPartition,创建 RecordReader 和 RecordWriter,创建算子实例。openOperators() 方法依次调用所有算子的 open() 方法初始化。processInputs() 方法是数据处理循环,从 RecordReader 读取数据,调用第一个算子的 processElement(),数据在算子链内依次传递。closeOperators() 方法依次调用所有算子的 close() 方法释放资源。releaseResources() 方法释放 RecordWriter、RecordReader、InputGate、ResultPartition 等资源。

3.4 RecordWriter 记录写入器

java 复制代码
public class RecordWriter<T> {
    private final ResultPartitionWriter targetPartition;
    private final ChannelSelector<T> channelSelector;
    private final Serializer<T> serializer;
    private final int numberOfChannels;

    public void emit(T record) throws IOException {
        // 1. 选择目标通道
        int channel = channelSelector.selectChannel(record);

        // 2. 序列化记录
        byte[] serialized = serializer.serialize(record);

        // 3. 写入 ResultPartition
        targetPartition.writeBuffer(serialized, channel);
    }

    public void broadcastEmit(T record) throws IOException {
        // 广播发送到所有通道
        byte[] serialized = serializer.serialize(record);
        for (int channel = 0; channel < numberOfChannels; channel++) {
            targetPartition.writeBuffer(serialized, channel);
        }
    }

    public void flush() throws IOException {
        // 刷新缓冲,发送数据
        targetPartition.flush();
    }

    public void close() throws IOException {
        // 关闭写入器
        flush();
        targetPartition.close();
    }
}

RecordWriter 是记录写入器,负责将算子输出的数据写入 ResultPartition。emit() 方法首先通过 ChannelSelector 选择目标通道(keyBy 时根据 key 的 hash 值选择通道),然后序列化记录,最后写入 ResultPartition。broadcastEmit() 方法广播发送到所有通道(broadcast 算子使用)。flush() 方法刷新缓冲,发送数据。close() 方法关闭写入器。RecordWriter 是算子输出的出口,数据通过 RecordWriter 写入 ResultPartition,再通过网络发送到下游 TaskManager。


四、数据传输与反压机制

下面这张图是数据传输与反压机制,包括 Pipelined vs Blocking 对比、网络缓冲机制、Credit-based 反压流程和配置最佳实践。

Pipelined vs Blocking 数据传输

Flink Task 执行中的数据传输有两种模式:

  • Pipelined(流水线传输):数据一条一条实时传输,上游处理完立即发送给下游。适用于流处理作业,低延迟要求,所有 Task 同时运行。需要所有 Task 同时运行,Slot 需求大,使用 Eager 调度。延迟低,毫秒级,数据实时流转。反压机制使用 Credit-based 反压,下游缓冲满时上游暂停发送。
  • Blocking(阻塞传输):上游全部处理完成后,批量发送给下游,数据落盘。适用于批处理作业,高吞吐要求,分阶段执行。只需要当前阶段的 Task,Slot 可复用,使用 Lazy 调度。延迟高,分钟级,需要等待上游完成。天然反压,上游完成后下游才开始,无需网络反压。

流处理作业默认使用 Pipelined 传输,批处理作业默认使用 Blocking 传输。可以通过 execution.shuffle-mode 配置指定 Shuffle 模式。

网络缓冲机制

网络缓冲机制分为5层:RecordWriter(记录写入器,序列化数据)→ ResultPartition(结果分区,管理输出缓冲)→ Netty Server/Client(网络服务,发送/接收数据)→ InputGate(输入网关,管理输入缓冲)→ RecordReader(记录读取器,反序列化数据)。数据从上游算子输出,经过 RecordWriter 序列化,写入 ResultPartition 缓冲,通过 Netty 网络发送到下游,下游 InputGate 接收数据,RecordReader 反序列化,输入到下游算子。

Credit-based 反压机制

Credit-based 反压是 Flink 的流控机制,流程5步:

  1. 下游缓冲满:InputGate 缓冲已满,无法接收更多数据
  2. 停止发送 Credit:下游停止向上游发送 Credit(信用量)
  3. 上游缓冲满:ResultPartition 缓冲已满,无法写入更多数据
  4. 上游暂停处理:RecordWriter 阻塞,算子暂停处理数据
  5. 反压传播:反压向上游传播,直到 Source 算子暂停读取数据

Credit-based 反压的核心思想是:下游告诉上游自己还有多少缓冲空间(Credit),上游只发送不超过 Credit 的数据。当下游缓冲满时,停止发送 Credit,上游无法发送数据,ResultPartition 缓冲满后 RecordWriter 阻塞,算子暂停处理,反压向上游传播。这种机制可以有效防止数据丢失和 OOM,保证系统稳定性。


五、状态管理与 Checkpoint

Task 执行过程中,状态管理是核心功能之一。Flink 的状态分为两种:

  • Keyed State(键控状态):按 key 分区的状态,每个 key 有独立的状态,如 ValueState、ListState、MapState、ReducingState、AggregatingState。Keyed State 只能在 keyBy 之后的算子中使用,状态按 key 自动分区和恢复。
  • Operator State(算子状态):与算子实例绑定的状态,不按 key 分区,如 ListState、UnionListState、BroadcastState。Operator State 适用于 Source/Sink 等非 keyBy 算子,状态在并行度调整时重新分配。

状态后端(StateBackend)负责状态的存储和恢复:

  • HashMapStateBackend:状态存储在 TaskManager 的堆内存中,读写快,但状态大小受内存限制,适合小状态场景。
  • EmbeddedRocksDBStateBackend:状态存储在 RocksDB 中(本地磁盘),支持大状态,读写需要序列化/反序列化,适合大状态场景。

Checkpoint 是 Flink 的容错机制,定期将状态快照持久化到 Checkpoint 存储(HDFS/S3)。Task 执行过程中,JobManager 触发 Checkpoint,Source 算子注入 Checkpoint Barrier,Barrier 随数据在算子间传递,算子收到 Barrier 后快照状态,发送 Barrier 到下游。所有算子完成快照后,Checkpoint 完成。故障恢复时,从最近的 Checkpoint 恢复状态,保证 Exactly-Once 语义。


六、关键配置参数

yaml 复制代码
# 网络内存占 TaskManager 总内存比例
taskmanager.memory.network.fraction: 0.1

# 每个通道独占缓冲数(Exclusive Buffers)
taskmanager.network.memory.buffers-per-channel: 2

# 每个 InputGate 浮动缓冲数(Floating Buffers)
taskmanager.network.memory.floating-buffers-per-gate: 8

# 网络缓冲大小
taskmanager.network.memory.buffer-size: 32KB

# 网络请求最大退避时间
taskmanager.network.request-backoff.max: 10s

# Shuffle 模式(pipelined/blocking/hybrid)
execution.shuffle-mode: pipelined

# Checkpoint 间隔
execution.checkpointing.interval: 1min

# 状态后端
state.backend: rocksdb

# 每个 TaskManager 的 Slot 数
taskmanager.numberOfTaskSlots: 1

# 默认并行度
parallelism.default: 1

关键配置说明:

  • taskmanager.memory.network.fraction:网络内存占 TaskManager 总内存比例,默认 10%,大流量作业可调大到 15-20%。
  • buffers-per-channel:每个通道独占缓冲数,默认 2,保证最低传输能力,高吞吐场景可调大。
  • floating-buffers-per-gate:每个 InputGate 浮动缓冲数,默认 8,在通道间动态分配,高吞吐场景可调大。
  • execution.shuffle-mode:Shuffle 模式,pipelined 流水线传输(流处理),blocking 阻塞传输(批处理),hybrid 混合模式。
  • state.backend:状态后端,hashmap 堆内存(小状态),rocksdb 本地磁盘(大状态)。

七、常见问题与最佳实践

常见问题

  1. open() 初始化失败

    • 原因:连接池创建失败、状态后端初始化异常、资源不足、广播变量加载失败。
    • 解决:检查外部系统连接,增加内存,查看 TM 日志,确认广播变量大小。
  2. 数据处理慢

    • 原因:数据倾斜、算子逻辑复杂、反压、GC 频繁、状态访问慢。
    • 解决:优化 keyBy 分区,简化算子逻辑,增加并行度,调优 GC,使用 RocksDB 状态后端。
  3. close() 资源泄漏

    • 原因:连接未关闭、文件句柄泄漏、状态未刷新、定时器未清理。
    • 解决:在 close() 中释放所有资源,使用 try-with-resources,确保状态刷新。
  4. Task 频繁失败

    • 原因:代码异常、OOM、Checkpoint 失败、网络异常、反压导致超时。
    • 解决:检查代码逻辑,增加内存,优化 Checkpoint,配置重启策略,调大超时。
  5. 反压严重

    • 原因:下游算子处理慢、网络带宽不足、缓冲配置不当、数据倾斜。
    • 解决:优化慢算子,增加网络内存,调整缓冲配置,解决数据倾斜。
  6. 状态过大

    • 原因:状态未清理、TTL 未配置、状态后端选择不当、Checkpoint 过大。
    • 解决:配置状态 TTL,使用 RocksDB 状态后端,增量 Checkpoint,清理无用状态。

最佳实践

  1. 使用算子链优化:将可以串联的算子(如 map→filter→map)合并到一个 Task 中执行,减少序列化和网络开销,提高性能。Flink 默认开启算子链优化,可通过 disableChaining() 禁用。

  2. 合理配置网络内存:大流量作业可调大 network.fraction 到 0.15-0.2,增加缓冲,减少反压。高吞吐场景增加 floating-buffers-per-gate,低延迟场景减少缓冲。

  3. 监控反压指标:关注 Web UI 反压指标(OK/LOW/HIGH),及时发现下游处理慢的算子。反压源头通常是慢算子,优化算子逻辑或增加并行度。

  4. 配置状态 TTL:为 Keyed State 配置 TTL(Time-To-Live),自动清理过期状态,防止状态无限增长。TTL 配置在 StateDescriptor 中,支持 processing time 和 event time。

  5. 选择合适的状态后端:小状态(<100MB)使用 HashMapStateBackend,读写快;大状态(>1GB)使用 EmbeddedRocksDBStateBackend,支持大状态,读写需要序列化。

  6. 开启增量 Checkpoint:大状态作业开启增量 Checkpoint,只快照变更的状态,减少 Checkpoint 时间和存储开销。RocksDB 状态后端支持增量 Checkpoint。

  7. 合理配置并行度:根据数据量和资源设置并行度,避免并行度过高导致调度开销大,或过低导致处理能力不足。Source 并行度受数据源分片数限制,可通过增加分片提高并行度。


八、总结

Flink Task 执行是一个6步流程:启动 Task 线程 → 创建 OperatorChain → 调用 open() 初始化算子 → processElement() 处理数据 → 调用 close() 释放资源 → 完成释放通知 JobMaster。核心类包括 Task(Runnable 实现,run 方法是核心执行逻辑)、TaskExecutor(Task 执行器,接收 submitTask,管理 Task 生命周期)、OperatorChain(算子链,串联多个算子,管理输入输出)、StreamTask(流任务,驱动数据处理循环)、RecordWriter(记录写入器,输出数据到 ResultPartition)、RecordReader(记录读取器,从 InputGate 读取数据)。

数据传输有两种模式:Pipelined(流水线传输,低延迟,适合流处理,Eager 调度)和 Blocking(阻塞传输,高吞吐,适合批处理,Lazy 调度)。网络缓冲机制分为5层:RecordWriter → ResultPartition → Netty Server/Client → InputGate → RecordReader。Credit-based 反压是 Flink 的流控机制,下游缓冲满时停止发送 Credit,上游 ResultPartition 缓冲满后 RecordWriter 阻塞,反压向上游传播直到 Source。

状态管理分为 Keyed State(按键分区)和 Operator State(算子实例绑定),状态后端有 HashMapStateBackend(堆内存,小状态)和 EmbeddedRocksDBStateBackend(本地磁盘,大状态)。Checkpoint 定期快照状态,故障恢复时从最近的 Checkpoint 恢复,保证 Exactly-Once 语义。

理解 Flink Task 执行的完整链路和核心类实现,有助于排查 open() 初始化失败、数据处理慢、close() 资源泄漏、Task 频繁失败、反压严重、状态过大等常见问题,为 Flink 作业的稳定运行提供保障。使用算子链优化、合理配置网络内存、监控反压指标、配置状态 TTL、选择合适的状态后端、开启增量 Checkpoint、合理配置并行度,可以显著提高作业的性能和稳定性。

相关推荐
KING-WU5122 小时前
Git 版本控制器实战指南
大数据·git·elasticsearch
SelectDB技术团队2 小时前
日志场景 Elasticsearch 替换实操:建表、索引、写入链路与查询改写的完整参数
大数据·数据库·数据仓库·elasticsearch·全文检索·日志分析·apache doris
大大大大晴天️2 小时前
每天认识一个组件:Spark Catalyst
大数据·spark
百数平台2 小时前
低代码安全技术实践:表单 / 流程 / 报表三级权限体系设计解析
java·大数据·数据库
一楼的猫2 小时前
第十一届中国教育后勤展览会杭州开幕 夏拾光影像为阿里、淘宝闪购提供云相册直播与视频服务 - 夏拾光影像
大数据·人工智能·音视频
程序 代码狂人3 小时前
金融数仓体系与核心业务知识详解
大数据·分布式·spark
shujudang3 小时前
金融行业数据分析平台选型:客户视图、转化诊断与权限管理
大数据·金融·数据挖掘·数据分析
Pin_Qing3 小时前
同一天,谷歌半价卖旗舰,阶跃翻倍收缓存
大数据·开发语言·人工智能·程序人生·缓存
Lalolander3 小时前
灵基Lingee怎么下载安装?Mac 与 Win 详细指南
大数据·人工智能·科技·macos·金蝶灵基