首页 文章 精选 留言 我的

精选列表

搜索[MLA 技术],共10007篇文章
优秀的个人博客,低调大师

【核心技术】Apache Flink CDC 批流融合技术原理分析

8月份 FlinkCDC 发布2.0.0版本,相较于1.0版本,在全量读取阶段支持分布式读取、支持checkpoint,且在全量 + 增量读取的过程在不锁表的情况下保障数据一致性。 详细介绍参考Flink CDC 2.0 正式发布,详解核心改进。 Flink CDC2.0 数据读取逻辑并不复杂,复杂的是FLIP-27: Refactor Source Interface的设计及对Debezium Api的不了解。本文重点对 Flink CDC 的处理逻辑进行介绍,FLIP-27的设计及 Debezium 的API调用不做过多讲解。 本文先以Flink SQL 案例来介绍Flink CDC2.0的使用,接着介绍CDC中的核心设计包含切片划分、切分读取、增量读取,最后对数据处理过程中涉及flink-mysql-cdc 接口的调用及实现进行代码讲解。 案例 全量读取+增量读取 Mysql表数据,以changelog-json格式写入kafka,观察 RowKind 类型及影响的数据条数。 public static void main(String[] args) { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); EnvironmentSettings envSettings = EnvironmentSettings.newInstance() .useBlinkPlanner() .inStreamingMode() .build(); env.setParallelism(3); // note: 增量同步需要开启CK env.enableCheckpointing(10000); StreamTableEnvironment tableEnvironment = StreamTableEnvironment.create(env, envSettings); tableEnvironment.executeSql(" CREATE TABLE demoOrders (\n" + " `order_id` INTEGER ,\n" + " `order_date` DATE ,\n" + " `order_time` TIMESTAMP(3),\n" + " `quantity` INT ,\n" + " `product_id` INT ,\n" + " `purchaser` STRING,\n" + " primary key(order_id) NOT ENFORCED" + " ) WITH (\n" + " 'connector' = 'mysql-cdc',\n" + " 'hostname' = 'localhost',\n" + " 'port' = '3306',\n" + " 'username' = 'cdc',\n" + " 'password' = '123456',\n" + " 'database-name' = 'test',\n" + " 'table-name' = 'demo_orders'," + // 全量 + 增量同步 " 'scan.startup.mode' = 'initial' " + " )"); tableEnvironment.executeSql("CREATE TABLE sink (\n" + " `order_id` INTEGER ,\n" + " `order_date` DATE ,\n" + " `order_time` TIMESTAMP(3),\n" + " `quantity` INT ,\n" + " `product_id` INT ,\n" + " `purchaser` STRING,\n" + " primary key (order_id) NOT ENFORCED " + ") WITH (\n" + " 'connector' = 'kafka',\n" + " 'properties.bootstrap.servers' = 'localhost:9092',\n" + " 'topic' = 'mqTest02',\n" + " 'format' = 'changelog-json' "+ ")"); tableEnvironment.executeSql("insert into sink select * from demoOrders");} 全量数据输出: {"data":{"order_id":1010,"order_date":"2021-09-17","order_time":"2021-09-22 10:52:12.189","quantity":53,"product_id":502,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1009,"order_date":"2021-09-17","order_time":"2021-09-22 10:52:09.709","quantity":31,"product_id":500,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1008,"order_date":"2021-09-17","order_time":"2021-09-22 10:52:06.637","quantity":69,"product_id":503,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1007,"order_date":"2021-09-17","order_time":"2021-09-22 10:52:03.535","quantity":52,"product_id":502,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1002,"order_date":"2021-09-17","order_time":"2021-09-22 10:51:51.347","quantity":69,"product_id":503,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1001,"order_date":"2021-09-17","order_time":"2021-09-22 10:51:48.783","quantity":50,"product_id":502,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1000,"order_date":"2021-09-17","order_time":"2021-09-17 17:40:32.354","quantity":30,"product_id":500,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1006,"order_date":"2021-09-17","order_time":"2021-09-22 10:52:01.249","quantity":31,"product_id":500,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1005,"order_date":"2021-09-17","order_time":"2021-09-22 10:51:58.813","quantity":69,"product_id":503,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1004,"order_date":"2021-09-17","order_time":"2021-09-22 10:51:56.153","quantity":50,"product_id":502,"purchaser":"flink"},"op":"+I"} {"data":{"order_id":1003,"order_date":"2021-09-17","order_time":"2021-09-22 10:51:53.727","quantity":30,"product_id":500,"purchaser":"flink"},"op":"+I"} ## 更新 1005 的值 {"data":{"order_id":1005,"order_date":"2021-09-17","order_time":"2021-09-22 02:51:58.813","quantity":69,"product_id":503,"purchaser":"flink"},"op":"-U"} {"data":{"order_id":1005,"order_date":"2021-09-17","order_time":"2021-09-22 02:55:43.627","quantity":80,"product_id":503,"purchaser":"flink"},"op":"+U"} ## 删除 1000 {"data":{"order_id":1000,"order_date":"2021-09-17","order_time":"2021-09-17 09:40:32.354","quantity":30,"product_id":500,"purchaser":"flink"},"op":"-D"} 核心设计 切片划分 全量阶段数据读取方式为分布式读取,会先对当前表数据按主键划分成多个Chunk,后续子任务读取Chunk 区间内的数据。根据主键列是否为自增整数类型,对表数据划分为均匀分布的Chunk及非均匀分布的Chunk。 均匀分布 主键列自增且类型为整数类型(int,bigint,decimal)。查询出主键列的最小值,最大值,按 chunkSize 大小将数据均匀划分,因为主键为整数类型,根据当前chunk 起始位置、chunkSize大小,直接计算chunk 的结束位置。 // 计算主键列数据区间 select min(`order_id`), max(`order_id`) from demo_orders; // 将数据划分为 chunkSize 大小的切片 chunk-0: [min,start + chunkSize) chunk-1: [start + chunkSize, start + 2chunkSize) ....... chunk-last: [max,null) 非均匀分布 主键列非自增或者类型为非整数类型。主键为非数值类型,每次划分需要对未划分的数据按主键进行升序排列,取出前 chunkSize 的最大值为当前 chunk 的结束位置。 // 未拆分的数据排序后,取 chunkSize 条数据取最大值,作为切片的终止位置。 chunkend = SELECT MAX(`order_id`) FROM ( SELECT `order_id` FROM `demo_orders` WHERE `order_id` &gt;= [前一个切片的起始位置] ORDER BY `order_id` ASC LIMIT [chunkSize] ) AS T 全量切片数据读取 Flink 将表数据划分为多个Chunk,子任务在不加锁的情况下,并行读取 Chunk数据。因为全程无锁在数据分片读取过程中,可能有其他事务对切片范围内的数据进行修改,此时无法保证数据一致性。因此,在全量阶段Flink 使用快照记录读取+Binlog数据修正的方式来保证数据的一致性。 快照读取 通过JDBC执行SQL查询切片范围的数据记录。 ## 快照记录数据读取SQL SELECT * FROM `test`.`demo_orders` WHERE order_id &gt;= [chunkStart] AND NOT (order_id = [chunkEnd]) AND order_id &lt;= [chunkEnd] 数据修正 在快照读取操作前、后执行SHOW MASTER STATUS查询binlog文件的当前偏移量,在快照读取完毕后,查询区间内的binlog数据并对读取的快照记录进行修正。 快照读取+Binlog数据读取时的数据组织结构。 BinlogEvents 修正 SnapshotEvents 规则。 未读取到binlog数据,即在执行select阶段没有其他事务进行操作,直接下发所有快照记录。 读取到binlog数据,且变更的数据记录不属于当前切片,下发快照记录。 读取到binlog数据,且数据记录的变更属于当前切片。delete 操作从快照内存中移除该数据,insert 操作向快照内存添加新的数据,update操作向快照内存中添加变更记录,最终会输出更新前后的两条记录到下游。 修正后的数据组织结构: 以读取切片 [1,11)范围的数据为例,描述切片数据的处理过程。c,d,u代表Debezium捕获到的新增、删除、更新操作。 修正前数据及结构: 修正后数据及结构: 单个切片数据处理完毕后会向 SplitEnumerator 发送已完成切片数据的起始位置(ChunkStart, ChunkStartEnd)、Binlog的最大偏移量(High watermark),用来为增量读取指定起始偏移量。 增量切片数据读取 全量阶段切片数据读取完成后,SplitEnumerator 会下发一个 BinlogSplit 进行增量数据读取。BinlogSplit读取最重要的属性就是起始偏移量,偏移量如果设置过小下游可能会有重复数据,偏移量如果设置过大下游可能是已超期的脏数据。而 Flink CDC增量读取的起始偏移量为所有已完成的全量切片最小的Binlog偏移量,只有满足条件的数据才被下发到下游。数据下发条件: 捕获的Binlog数据的偏移量 > 数据所属分片的Binlog的最大偏移量。 例如,SplitEnumerator 保留的已完成切片信息为。 切片索引 Chunk 数据范围 切片读取的最大Binlog 0 [1,100] 1000 1 [101,200] 800 2 [201,300] 1500 增量读取时,从偏移量 800 开始读取Binlog数据 ,当捕获到数据 <data:123, offset:1500> 时,先找到 123 所属快照分片,并找到对应的最大Binlog 偏移量 800。 当前偏移量大于快照读的最大偏移量,则下发数据,否则直接丢弃。 代码详解 关于FLIP-27: Refactor Source Interface设计不做详细介绍,本文侧重对 flink-mysql-cdc 接口调用及实现进行讲解。 MySqlSourceEnumerator 初始化 SourceCoordinator作为OperatorCoordinator对Source的实现,运行在Master节点,在启动时通过调用MySqlParallelSource#createEnumerator 创建 MySqlSourceEnumerator 并调用start方法,做一些初始化工作。 创建 MySqlSourceEnumerator,使用 MySqlHybridSplitAssigner 对全量+增量数据进行切片,使用 MySqlValidator 对 mysql 版本、配置进行校验。 MySqlValidator 校验: mysql版本必须大于等于5.7。 binlog_format 配置必须为 ROW。 binlog_row_image 配置必须为 FULL。 MySqlSplitAssigner 初始化: 创建 ChunkSplitter用来划分切片。 筛选出要读的表名称。 启动周期调度线程,要求 SourceReader 向 SourceEnumerator 发送已完成但未发送ACK事件的切片信息。 private void syncWithReaders(int[] subtaskIds, Throwable t) { if (t != null) { throw new FlinkRuntimeException("Failed to list obtain registered readers due to:", t); } // when the SourceEnumerator restores or the communication failed between // SourceEnumerator and SourceReader, it may missed some notification event. // tell all SourceReader(s) to report there finished but unacked splits. if (splitAssigner.waitingForFinishedSplits()) { for (int subtaskId : subtaskIds) { // note: 发送 FinishedSnapshotSplitsRequestEvent context.sendEventToSourceReader( subtaskId, new FinishedSnapshotSplitsRequestEvent()); } } } MySqlSourceReader 初始化 SourceOperator 集成了SourceReader,通过OperatorEventGateway 和 SourceCoordinator 进行交互。 SourceOperator 在初始化时,通过 MySqlParallelSource 创建 MySqlSourceReader。MySqlSourceReader 通过 SingleThreadFetcherManager 创建Fetcher拉取分片数据,数据以 MySqlRecords 格式写入到 elementsQueue。 MySqlParallelSource#createReader public SourceReader&lt;T, MySqlSplit&gt; createReader(SourceReaderContext readerContext) throws Exception { // note: 数据存储队列 FutureCompletingBlockingQueue&lt;RecordsWithSplitIds&lt;SourceRecord&gt;&gt; elementsQueue = new FutureCompletingBlockingQueue&lt;&gt;(); final Configuration readerConfiguration = getReaderConfig(readerContext); // note: Split Reader 工厂类 Supplier&lt;MySqlSplitReader&gt; splitReaderSupplier = () -&gt; new MySqlSplitReader(readerConfiguration, readerContext.getIndexOfSubtask()); return new MySqlSourceReader&lt;&gt;( elementsQueue, splitReaderSupplier, new MySqlRecordEmitter&lt;&gt;(deserializationSchema), readerConfiguration, readerContext); } 将创建的 MySqlSourceReader 以事件的形式传递给 SourceCoordinator 进行注册。SourceCoordinator 接收到注册事件后,将reader 地址及索引进行保存。 SourceCoordinator#handleReaderRegistrationEvent // note: SourceCoordinator 处理Reader 注册事件 private void handleReaderRegistrationEvent(ReaderRegistrationEvent event) { context.registerSourceReader(new ReaderInfo(event.subtaskId(), event.location())); enumerator.addReader(event.subtaskId()); } MySqlSourceReader 启动后会向 MySqlSourceEnumerator 发送请求分片事件,从而收集分配的切片数据。 SourceOperator 初始化完毕后,调用 emitNext 由 SourceReaderBase 从 elementsQueue 获取数据集合并下发给 MySqlRecordEmitter。接口调用示意图:! MySqlSourceEnumerator 处理分片请求 MySqlSourceReader 启动时会向 MySqlSourceEnumerator 发送请求 RequestSplitEvent 事件,根据返回的切片范围读取区间数据。MySqlSourceEnumerator 全量读取阶段分片请求处理逻辑,最终返回一个MySqlSnapshotSplit。 处理切片请求事件,为请求的Reader分配切片,通过发送AddSplitEvent时间传递MySqlSplit(全量阶段MySqlSnapshotSplit、增量阶段MySqlBinlogSplit)。 MySqlSourceEnumerator#handleSplitRequest public void handleSplitRequest(int subtaskId, @Nullable String requesterHostname) { if (!context.registeredReaders().containsKey(subtaskId)) { // reader failed between sending the request and now. skip this request. return; } // note: 将reader所属的subtaskId存储到TreeSet, 在处理binlog split时优先分配个task-0 readersAwaitingSplit.add(subtaskId); assignSplits(); } // note: 分配切片 private void assignSplits() { final Iterator&lt;Integer&gt; awaitingReader = readersAwaitingSplit.iterator(); while (awaitingReader.hasNext()) { int nextAwaiting = awaitingReader.next(); // if the reader that requested another split has failed in the meantime, remove // it from the list of waiting readers if (!context.registeredReaders().containsKey(nextAwaiting)) { awaitingReader.remove(); continue; } //note: 由 MySqlSplitAssigner 分配切片 Optional&lt;MySqlSplit&gt; split = splitAssigner.getNext(); if (split.isPresent()) { final MySqlSplit mySqlSplit = split.get(); // note: 发送AddSplitEvent, 为 Reader 返回切片信息 context.assignSplit(mySqlSplit, nextAwaiting); awaitingReader.remove(); LOG.info("Assign split {} to subtask {}", mySqlSplit, nextAwaiting); } else { // there is no available splits by now, skip assigning break; } } } MySqlHybridSplitAssigner 处理全量切片、增量切片的逻辑。 任务刚启动时,remainingTables不为空,noMoreSplits返回值为false,创建 SnapshotSplit。 全量阶段分片读取完成后,noMoreSplits返回值为true, 创建 BinlogSplit。 MySqlHybridSplitAssigner#getNext @Override public Optional&lt;MySqlSplit&gt; getNext() { if (snapshotSplitAssigner.noMoreSplits()) { // binlog split assigning if (isBinlogSplitAssigned) { // no more splits for the assigner return Optional.empty(); } else if (snapshotSplitAssigner.isFinished()) { // we need to wait snapshot-assigner to be finished before // assigning the binlog split. Otherwise, records emitted from binlog split // might be out-of-order in terms of same primary key with snapshot splits. isBinlogSplitAssigned = true; //note: snapshot split 切片完成后,创建BinlogSplit。 return Optional.of(createBinlogSplit()); } else { // binlog split is not ready by now return Optional.empty(); } } else { // note: 由MySqlSnapshotSplitAssigner 创建 SnapshotSplit // snapshot assigner still have remaining splits, assign split from it return snapshotSplitAssigner.getNext(); } } MySqlSnapshotSplitAssigner 处理全量切片逻辑,通过 ChunkSplitter 生成切片,并存储到Iterator中。 @Override public Optional&lt;MySqlSplit&gt; getNext() { if (!remainingSplits.isEmpty()) { // return remaining splits firstly Iterator&lt;MySqlSnapshotSplit&gt; iterator = remainingSplits.iterator(); MySqlSnapshotSplit split = iterator.next(); iterator.remove(); //note: 已分配的切片存储到 assignedSplits 集合 assignedSplits.put(split.splitId(), split); return Optional.of(split); } else { // note: 初始化阶段 remainingTables 存储了要读取的表名 TableId nextTable = remainingTables.pollFirst(); if (nextTable != null) { // split the given table into chunks (snapshot splits) // note: 初始化阶段创建了 ChunkSplitter,调用generateSplits 进行切片划分 Collection&lt;MySqlSnapshotSplit&gt; splits = chunkSplitter.generateSplits(nextTable); // note: 保留所有切片信息 remainingSplits.addAll(splits); // note: 已经完成分片的 Table alreadyProcessedTables.add(nextTable); // note: 递归调用该该方法 return getNext(); } else { return Optional.empty(); } } } 4.ChunkSplitter 将表划分为均匀分布 or 不均匀分布切片的逻辑。读取的表必须包含物理主键。 public Collection&lt;MySqlSnapshotSplit&gt; generateSplits(TableId tableId) { Table schema = mySqlSchema.getTableSchema(tableId).getTable(); List&lt;Column&gt; primaryKeys = schema.primaryKeyColumns(); // note: 必须有主键 if (primaryKeys.isEmpty()) { throw new ValidationException( String.format( "Incremental snapshot for tables requires primary key," + " but table %s doesn't have primary key.", tableId)); } // use first field in primary key as the split key Column splitColumn = primaryKeys.get(0); final List&lt;ChunkRange&gt; chunks; try { // note: 按主键列将数据划分成多个切片 chunks = splitTableIntoChunks(tableId, splitColumn); } catch (SQLException e) { throw new FlinkRuntimeException("Failed to split chunks for table " + tableId, e); } //note: 主键数据类型转换、ChunkRange 包装成MySqlSnapshotSplit。 // convert chunks into splits List&lt;MySqlSnapshotSplit&gt; splits = new ArrayList&lt;&gt;(); RowType splitType = splitType(splitColumn); for (int i = 0; i &lt; chunks.size(); i++) { ChunkRange chunk = chunks.get(i); MySqlSnapshotSplit split = createSnapshotSplit( tableId, i, splitType, chunk.getChunkStart(), chunk.getChunkEnd()); splits.add(split); } return splits; } splitTableIntoChunks 根据物理主键划分切片。 private List&lt;ChunkRange&gt; splitTableIntoChunks(TableId tableId, Column splitColumn) throws SQLException { final String splitColumnName = splitColumn.name(); // select min, max final Object[] minMaxOfSplitColumn = queryMinMax(jdbc, tableId, splitColumnName); final Object min = minMaxOfSplitColumn[0]; final Object max = minMaxOfSplitColumn[1]; if (min == null || max == null || min.equals(max)) { // empty table, or only one row, return full table scan as a chunk return Collections.singletonList(ChunkRange.all()); } final List&lt;ChunkRange&gt; chunks; if (splitColumnEvenlyDistributed(splitColumn)) { // use evenly-sized chunks which is much efficient // note: 按主键均匀划分 chunks = splitEvenlySizedChunks(min, max); } else { // note: 按主键非均匀划分 // use unevenly-sized chunks which will request many queries and is not efficient. chunks = splitUnevenlySizedChunks(tableId, splitColumnName, min, max); } return chunks; } /** Checks whether split column is evenly distributed across its range. */ private static boolean splitColumnEvenlyDistributed(Column splitColumn) { // only column is auto-incremental are recognized as evenly distributed. // TODO: we may use MAX,MIN,COUNT to calculate the distribution in the future. if (splitColumn.isAutoIncremented()) { DataType flinkType = MySqlTypeUtils.fromDbzColumn(splitColumn); LogicalTypeRoot typeRoot = flinkType.getLogicalType().getTypeRoot(); // currently, we only support split column with type BIGINT, INT, DECIMAL return typeRoot == LogicalTypeRoot.BIGINT || typeRoot == LogicalTypeRoot.INTEGER || typeRoot == LogicalTypeRoot.DECIMAL; } else { return false; } } /** * 根据拆分列的最小值和最大值将表拆分为大小均匀的块,并以 {@link #chunkSize} 步长滚动块。 * Split table into evenly sized chunks based on the numeric min and max value of split column, * and tumble chunks in {@link #chunkSize} step size. */ private List&lt;ChunkRange&gt; splitEvenlySizedChunks(Object min, Object max) { if (ObjectUtils.compare(ObjectUtils.plus(min, chunkSize), max) &gt; 0) { // there is no more than one chunk, return full table as a chunk return Collections.singletonList(ChunkRange.all()); } final List&lt;ChunkRange&gt; splits = new ArrayList&lt;&gt;(); Object chunkStart = null; Object chunkEnd = ObjectUtils.plus(min, chunkSize); // chunkEnd &lt;= max while (ObjectUtils.compare(chunkEnd, max) &lt;= 0) { splits.add(ChunkRange.of(chunkStart, chunkEnd)); chunkStart = chunkEnd; chunkEnd = ObjectUtils.plus(chunkEnd, chunkSize); } // add the ending split splits.add(ChunkRange.of(chunkStart, null)); return splits; } /** 通过连续计算下一个块最大值,将表拆分为大小不均匀的块。 * Split table into unevenly sized chunks by continuously calculating next chunk max value. */ private List&lt;ChunkRange&gt; splitUnevenlySizedChunks( TableId tableId, String splitColumnName, Object min, Object max) throws SQLException { final List&lt;ChunkRange&gt; splits = new ArrayList&lt;&gt;(); Object chunkStart = null; Object chunkEnd = nextChunkEnd(min, tableId, splitColumnName, max); int count = 0; while (chunkEnd != null && ObjectUtils.compare(chunkEnd, max) &lt;= 0) { // we start from [null, min + chunk_size) and avoid [null, min) splits.add(ChunkRange.of(chunkStart, chunkEnd)); // may sleep a while to avoid DDOS on MySQL server maySleep(count++); chunkStart = chunkEnd; chunkEnd = nextChunkEnd(chunkEnd, tableId, splitColumnName, max); } // add the ending split splits.add(ChunkRange.of(chunkStart, null)); return splits; } private Object nextChunkEnd( Object previousChunkEnd, TableId tableId, String splitColumnName, Object max) throws SQLException { // chunk end might be null when max values are removed Object chunkEnd = queryNextChunkMax(jdbc, tableId, splitColumnName, chunkSize, previousChunkEnd); if (Objects.equals(previousChunkEnd, chunkEnd)) { // we don't allow equal chunk start and end, // should query the next one larger than chunkEnd chunkEnd = queryMin(jdbc, tableId, splitColumnName, chunkEnd); } if (ObjectUtils.compare(chunkEnd, max) &gt;= 0) { return null; } else { return chunkEnd; } } MySqlSourceReader 处理切片分配请求 MySqlSourceReader接收到切片分配请求后,会为先创建一个 SplitFetcher线程,向 taskQueue 添加、执行AddSplitsTask 任务用来处理添加分片任务,接着执行 FetchTask 使用Debezium API进行读取数据,读取的数据存储到 elementsQueue** **中,SourceReaderBase 会从该队列中获取数据,并下发给 MySqlRecordEmitter。 ​ 处理切片分配事件时,创建SplitFetcher向taskQueue添加AddSplitsTask。 SingleThreadFetcherManager#addSplits public void addSplits(List&lt;SplitT&gt; splitsToAdd) { SplitFetcher&lt;E, SplitT&gt; fetcher = getRunningFetcher(); if (fetcher == null) { fetcher = createSplitFetcher(); // Add the splits to the fetchers. fetcher.addSplits(splitsToAdd); startFetcher(fetcher); } else { fetcher.addSplits(splitsToAdd); } } // 创建 SplitFetcher protected synchronized SplitFetcher&lt;E, SplitT&gt; createSplitFetcher() { if (closed) { throw new IllegalStateException("The split fetcher manager has closed."); } // Create SplitReader. SplitReader&lt;E, SplitT&gt; splitReader = splitReaderFactory.get(); int fetcherId = fetcherIdGenerator.getAndIncrement(); SplitFetcher&lt;E, SplitT&gt; splitFetcher = new SplitFetcher&lt;&gt;( fetcherId, elementsQueue, splitReader, errorHandler, () -&gt; { fetchers.remove(fetcherId); elementsQueue.notifyAvailable(); }); fetchers.put(fetcherId, splitFetcher); return splitFetcher; } public void addSplits(List&lt;SplitT&gt; splitsToAdd) { enqueueTask(new AddSplitsTask&lt;&gt;(splitReader, splitsToAdd, assignedSplits)); wakeUp(true); } 执行 SplitFetcher线程,首次执行 AddSplitsTask 线程添加分片,以后执行 FetchTask 线程拉取数据。 SplitFetcher#runOnce void runOnce() { try { if (shouldRunFetchTask()) { runningTask = fetchTask; } else { runningTask = taskQueue.take(); } if (!wakeUp.get() && runningTask.run()) { LOG.debug("Finished running task {}", runningTask); runningTask = null; checkAndSetIdle(); } } catch (Exception e) { throw new RuntimeException( String.format( "SplitFetcher thread %d received unexpected exception while polling the records", id), e); } maybeEnqueueTask(runningTask); synchronized (wakeUp) { // Set the running task to null. It is necessary for the shutdown method to avoid // unnecessarily interrupt the running task. runningTask = null; // Set the wakeUp flag to false. wakeUp.set(false); LOG.debug("Cleaned wakeup flag."); } } AddSplitsTask 调用 MySqlSplitReader 的 handleSplitsChanges方法,向切片队列中添加已分配的切片信息。在下一次fetch()调用时,从队列中获取切片并读取切片数据。 AddSplitsTask#run public boolean run() { for (SplitT s : splitsToAdd) { assignedSplits.put(s.splitId(), s); } splitReader.handleSplitsChanges(new SplitsAddition&lt;&gt;(splitsToAdd)); return true; } MySqlSplitReader#handleSplitsChanges public void handleSplitsChanges(SplitsChange&lt;MySqlSplit&gt; splitsChanges) { if (!(splitsChanges instanceof SplitsAddition)) { throw new UnsupportedOperationException( String.format( "The SplitChange type of %s is not supported.", splitsChanges.getClass())); } //note: 添加切片 到队列。 splits.addAll(splitsChanges.splits()); } MySqlSplitReader 执行fetch(),由DebeziumReader读取数据到事件队列,在对数据修正后以MySqlRecords格式返回。 MySqlSplitReader#fetch @Override public RecordsWithSplitIds&lt;SourceRecord&gt; fetch() throws IOException { // note: 创建Reader 并读取数据 checkSplitOrStartNext(); Iterator&lt;SourceRecord&gt; dataIt = null; try { // note: 对读取的数据进行修正 dataIt = currentReader.pollSplitRecords(); } catch (InterruptedException e) { LOG.warn("fetch data failed.", e); throw new IOException(e); } // note: 返回的数据被封装为 MySqlRecords 进行传输 return dataIt == null ? finishedSnapshotSplit() : MySqlRecords.forRecords(currentSplitId, dataIt); } private void checkSplitOrStartNext() throws IOException { // the binlog reader should keep alive if (currentReader instanceof BinlogSplitReader) { return; } if (canAssignNextSplit()) { // note: 从切片队列读取MySqlSplit final MySqlSplit nextSplit = splits.poll(); if (nextSplit == null) { throw new IOException("Cannot fetch from another split - no split remaining"); } currentSplitId = nextSplit.splitId(); // note: 区分全量切片读取还是增量切片读取 if (nextSplit.isSnapshotSplit()) { if (currentReader == null) { final MySqlConnection jdbcConnection = getConnection(config); final BinaryLogClient binaryLogClient = getBinaryClient(config); final StatefulTaskContext statefulTaskContext = new StatefulTaskContext(config, binaryLogClient, jdbcConnection); // note: 创建SnapshotSplitReader,使用Debezium Api读取分配数据及区间Binlog值 currentReader = new SnapshotSplitReader(statefulTaskContext, subtaskId); } } else { // point from snapshot split to binlog split if (currentReader != null) { LOG.info("It's turn to read binlog split, close current snapshot reader"); currentReader.close(); } final MySqlConnection jdbcConnection = getConnection(config); final BinaryLogClient binaryLogClient = getBinaryClient(config); final StatefulTaskContext statefulTaskContext = new StatefulTaskContext(config, binaryLogClient, jdbcConnection); LOG.info("Create binlog reader"); // note: 创建BinlogSplitReader,使用Debezium API进行增量读取 currentReader = new BinlogSplitReader(statefulTaskContext, subtaskId); } // note: 执行Reader进行数据读取 currentReader.submitSplit(nextSplit); } } DebeziumReader 数据处理 DebeziumReader 包含全量切片读取、增量切片读取两个阶段,数据读取后存储到 ChangeEventQueue,执行pollSplitRecords 时对数据进行修正。 SnapshotSplitReader 全量切片读取。全量阶段的数据读取通过执行Select语句查询出切片范围内的表数据,在写入队列前后执行 SHOW MASTER STATUS 时,写入当前偏移量。 public void submitSplit(MySqlSplit mySqlSplit) { ...... executor.submit( () -&gt; { try { currentTaskRunning = true; // note: 数据读取,在数据前后插入Binlog当前偏移量 // 1. execute snapshot read task。 final SnapshotSplitChangeEventSourceContextImpl sourceContext = new SnapshotSplitChangeEventSourceContextImpl(); SnapshotResult snapshotResult = splitSnapshotReadTask.execute(sourceContext); // note: 为增量读取做准备,包含了起始偏移量 final MySqlBinlogSplit appendBinlogSplit = createBinlogSplit(sourceContext); final MySqlOffsetContext mySqlOffsetContext = statefulTaskContext.getOffsetContext(); mySqlOffsetContext.setBinlogStartPoint( appendBinlogSplit.getStartingOffset().getFilename(), appendBinlogSplit.getStartingOffset().getPosition()); // note: 从起始偏移量开始读取 // 2. execute binlog read task if (snapshotResult.isCompletedOrSkipped()) { // we should only capture events for the current table, Configuration dezConf = statefulTaskContext .getDezConf() .edit() .with( "table.whitelist", currentSnapshotSplit.getTableId()) .build(); // task to read binlog for current split MySqlBinlogSplitReadTask splitBinlogReadTask = new MySqlBinlogSplitReadTask( new MySqlConnectorConfig(dezConf), mySqlOffsetContext, statefulTaskContext.getConnection(), statefulTaskContext.getDispatcher(), statefulTaskContext.getErrorHandler(), StatefulTaskContext.getClock(), statefulTaskContext.getTaskContext(), (MySqlStreamingChangeEventSourceMetrics) statefulTaskContext .getStreamingChangeEventSourceMetrics(), statefulTaskContext .getTopicSelector() .getPrimaryTopic(), appendBinlogSplit); splitBinlogReadTask.execute( new SnapshotBinlogSplitChangeEventSourceContextImpl()); } else { readException = new IllegalStateException( String.format( "Read snapshot for mysql split %s fail", currentSnapshotSplit)); } } catch (Exception e) { currentTaskRunning = false; LOG.error( String.format( "Execute snapshot read task for mysql split %s fail", currentSnapshotSplit), e); readException = e; } }); } SnapshotSplitReader 增量切片读取。增量阶段切片读取重点是判断BinlogSplitReadTask什么时候停止,在读取到分片阶段的结束时的偏移量即终止。 MySqlBinlogSplitReadTask#handleEvent protected void handleEvent(Event event) { // note: 事件下发 队列 super.handleEvent(event); // note: 全量读取阶段需要终止Binlog读取 // check do we need to stop for read binlog for snapshot split. if (isBoundedRead()) { final BinlogOffset currentBinlogOffset = new BinlogOffset( offsetContext.getOffset().get(BINLOG_FILENAME_OFFSET_KEY).toString(), Long.parseLong( offsetContext .getOffset() .get(BINLOG_POSITION_OFFSET_KEY) .toString())); // note: currentBinlogOffset &gt; HW 停止读取 // reach the high watermark, the binlog reader should finished if (currentBinlogOffset.isAtOrBefore(binlogSplit.getEndingOffset())) { // send binlog end event try { signalEventDispatcher.dispatchWatermarkEvent( binlogSplit, currentBinlogOffset, SignalEventDispatcher.WatermarkKind.BINLOG_END); } catch (InterruptedException e) { logger.error("Send signal event error.", e); errorHandler.setProducerThrowable( new DebeziumException("Error processing binlog signal event", e)); } // 终止binlog读取 // tell reader the binlog task finished ((SnapshotBinlogSplitChangeEventSourceContextImpl) context).finished(); } } } SnapshotSplitReader 执行pollSplitRecords 时对队列中的原始数据进行修正。 具体处理逻辑查看 RecordUtils#normalizedSplitRecords。 public Iterator&lt;SourceRecord&gt; pollSplitRecords() throws InterruptedException { if (hasNextElement.get()) { // data input: [low watermark event][snapshot events][high watermark event][binlogevents][binlog-end event] // data output: [low watermark event][normalized events][high watermark event] boolean reachBinlogEnd = false; final List&lt;SourceRecord&gt; sourceRecords = new ArrayList&lt;&gt;(); while (!reachBinlogEnd) { // note: 处理队列中写入的 DataChangeEvent 事件 List&lt;DataChangeEvent&gt; batch = queue.poll(); for (DataChangeEvent event : batch) { sourceRecords.add(event.getRecord()); if (RecordUtils.isEndWatermarkEvent(event.getRecord())) { reachBinlogEnd = true; break; } } } // snapshot split return its data once hasNextElement.set(false); // ************ 修正数据 *********** return normalizedSplitRecords(currentSnapshotSplit, sourceRecords, nameAdjuster) .iterator(); } // the data has been polled, no more data reachEnd.compareAndSet(false, true); return null; } BinlogSplitReader 数据读取。读取逻辑比较简单,重点是起始偏移量的设置,起始偏移量为所有切片的HW。 BinlogSplitReader 执行pollSplitRecords 时对队列中的原始数据进行修正,保障数据一致性。 增量阶段的Binlog读取是无界的,数据会全部下发到事件队列,BinlogSplitReader 通过shouldEmit()判断数据是否下发。 BinlogSplitReader#pollSplitRecords public Iterator&lt;SourceRecord&gt; pollSplitRecords() throws InterruptedException { checkReadException(); final List&lt;SourceRecord&gt; sourceRecords = new ArrayList&lt;&gt;(); if (currentTaskRunning) { List&lt;DataChangeEvent&gt; batch = queue.poll(); for (DataChangeEvent event : batch) { if (shouldEmit(event.getRecord())) { sourceRecords.add(event.getRecord()); } } } return sourceRecords.iterator(); } 事件下发条件:1. 新收到的event post 大于 maxwm 2. 当前 data值所属某个snapshot spilt & 偏移量大于 HWM,下发数据。 /** * * Returns the record should emit or not. * * The watermark signal algorithm is the binlog split reader only sends the binlog event that * belongs to its finished snapshot splits. For each snapshot split, the binlog event is valid * since the offset is after its high watermark. * * E.g: the data input is : * snapshot-split-0 info : [0, 1024) highWatermark0 * snapshot-split-1 info : [1024, 2048) highWatermark1 * the data output is: * only the binlog event belong to [0, 1024) and offset is after highWatermark0 should send, * only the binlog event belong to [1024, 2048) and offset is after highWatermark1 should send. * */ private boolean shouldEmit(SourceRecord sourceRecord) { if (isDataChangeRecord(sourceRecord)) { TableId tableId = getTableId(sourceRecord); BinlogOffset position = getBinlogPosition(sourceRecord); // aligned, all snapshot splits of the table has reached max highWatermark // note: 新收到的event post 大于 maxwm ,直接下发 if (position.isAtOrBefore(maxSplitHighWatermarkMap.get(tableId))) { return true; } Object[] key = getSplitKey( currentBinlogSplit.getSplitKeyType(), sourceRecord, statefulTaskContext.getSchemaNameAdjuster()); for (FinishedSnapshotSplitInfo splitInfo : finishedSplitsInfo.get(tableId)) { /** * note: 当前 data值所属某个snapshot spilt & 偏移量大于 HWM,下发数据 */ if (RecordUtils.splitKeyRangeContains( key, splitInfo.getSplitStart(), splitInfo.getSplitEnd()) && position.isAtOrBefore(splitInfo.getHighWatermark())) { return true; } } // not in the monitored splits scope, do not emit return false; } // always send the schema change event and signal event // we need record them to state of Flink return true; } MySqlRecordEmitter 数据下发 SourceReaderBase 从队列中获取切片读取的DataChangeEvent数据集合,将数据类型由Debezium的DataChangeEvent 转换为Flink 的RowData类型。 SourceReaderBase 处理切片数据流程。 org.apache.flink.connector.base.source.reader.SourceReaderBase#pollNext public InputStatus pollNext(ReaderOutput&lt;T&gt; output) throws Exception { // make sure we have a fetch we are working on, or move to the next RecordsWithSplitIds&lt;E&gt; recordsWithSplitId = this.currentFetch; if (recordsWithSplitId == null) { recordsWithSplitId = getNextFetch(output); if (recordsWithSplitId == null) { return trace(finishedOrAvailableLater()); } } // we need to loop here, because we may have to go across splits while (true) { // Process one record. // note: 通过MySqlRecords从迭代器中读取单条数据 final E record = recordsWithSplitId.nextRecordFromSplit(); if (record != null) { // emit the record. recordEmitter.emitRecord(record, currentSplitOutput, currentSplitContext.state); LOG.trace("Emitted record: {}", record); // We always emit MORE_AVAILABLE here, even though we do not strictly know whether // more is available. If nothing more is available, the next invocation will find // this out and return the correct status. // That means we emit the occasional 'false positive' for availability, but this // saves us doing checks for every record. Ultimately, this is cheaper. return trace(InputStatus.MORE_AVAILABLE); } else if (!moveToNextSplit(recordsWithSplitId, output)) { // The fetch is done and we just discovered that and have not emitted anything, yet. // We need to move to the next fetch. As a shortcut, we call pollNext() here again, // rather than emitting nothing and waiting for the caller to call us again. return pollNext(output); } // else fall through the loop } } private RecordsWithSplitIds&lt;E&gt; getNextFetch(final ReaderOutput&lt;T&gt; output) { splitFetcherManager.checkErrors(); LOG.trace("Getting next source data batch from queue"); // note: 从elementsQueue 获取数据 final RecordsWithSplitIds&lt;E&gt; recordsWithSplitId = elementsQueue.poll(); if (recordsWithSplitId == null || !moveToNextSplit(recordsWithSplitId, output)) { return null; } currentFetch = recordsWithSplitId; return recordsWithSplitId; } MySqlRecords 返回单条数据集合。 com.ververica.cdc.connectors.mysql.source.split.MySqlRecords#nextRecordFromSplit public SourceRecord nextRecordFromSplit() { final Iterator&lt;SourceRecord&gt; recordsForSplit = this.recordsForCurrentSplit; if (recordsForSplit != null) { if (recordsForSplit.hasNext()) { return recordsForSplit.next(); } else { return null; } } else { throw new IllegalStateException(); } } MySqlRecordEmitter 通过 RowDataDebeziumDeserializeSchema 将数据转换为Rowdata。 com.ververica.cdc.connectors.mysql.source.reader.MySqlRecordEmitter#emitRecord public void emitRecord(SourceRecord element, SourceOutput&lt;T&gt; output, MySqlSplitState splitState) throws Exception { if (isWatermarkEvent(element)) { BinlogOffset watermark = getWatermark(element); if (isHighWatermarkEvent(element) && splitState.isSnapshotSplitState()) { splitState.asSnapshotSplitState().setHighWatermark(watermark); } } else if (isSchemaChangeEvent(element) && splitState.isBinlogSplitState()) { HistoryRecord historyRecord = getHistoryRecord(element); Array tableChanges = historyRecord.document().getArray(HistoryRecord.Fields.TABLE_CHANGES); TableChanges changes = TABLE_CHANGE_SERIALIZER.deserialize(tableChanges, true); for (TableChanges.TableChange tableChange : changes) { splitState.asBinlogSplitState().recordSchema(tableChange.getId(), tableChange); } } else if (isDataChangeRecord(element)) { // note: 数据的处理 if (splitState.isBinlogSplitState()) { BinlogOffset position = getBinlogPosition(element); splitState.asBinlogSplitState().setStartingOffset(position); } debeziumDeserializationSchema.deserialize( element, new Collector&lt;T&gt;() { @Override public void collect(final T t) { output.collect(t); } @Override public void close() { // do nothing } }); } else { // unknown element LOG.info("Meet unknown element {}, just skip.", element); } } RowDataDebeziumDeserializeSchema 序列化过程。 com.ververica.cdc.debezium.table.RowDataDebeziumDeserializeSchema#deserialize public void deserialize(SourceRecord record, Collector&lt;RowData&gt; out) throws Exception { Envelope.Operation op = Envelope.operationFor(record); Struct value = (Struct) record.value(); Schema valueSchema = record.valueSchema(); if (op == Envelope.Operation.CREATE || op == Envelope.Operation.READ) { GenericRowData insert = extractAfterRow(value, valueSchema); validator.validate(insert, RowKind.INSERT); insert.setRowKind(RowKind.INSERT); out.collect(insert); } else if (op == Envelope.Operation.DELETE) { GenericRowData delete = extractBeforeRow(value, valueSchema); validator.validate(delete, RowKind.DELETE); delete.setRowKind(RowKind.DELETE); out.collect(delete); } else { GenericRowData before = extractBeforeRow(value, valueSchema); validator.validate(before, RowKind.UPDATE_BEFORE); before.setRowKind(RowKind.UPDATE_BEFORE); out.collect(before); GenericRowData after = extractAfterRow(value, valueSchema); validator.validate(after, RowKind.UPDATE_AFTER); after.setRowKind(RowKind.UPDATE_AFTER); out.collect(after); } } MySqlSourceReader 汇报切片读取完成事件 MySqlSourceReader处理完一个全量切片后,会向MySqlSourceEnumerator发送已完成的切片信息,包含切片ID、HighWatermar ,然后继续发送切片请求。 com.ververica.cdc.connectors.mysql.source.reader.MySqlSourceReader#onSplitFinished protected void onSplitFinished(Map&lt;String, MySqlSplitState&gt; finishedSplitIds) { for (MySqlSplitState mySqlSplitState : finishedSplitIds.values()) { MySqlSplit mySqlSplit = mySqlSplitState.toMySqlSplit(); finishedUnackedSplits.put(mySqlSplit.splitId(), mySqlSplit.asSnapshotSplit()); } /** * note: 发送切片完成事件 */ reportFinishedSnapshotSplitsIfNeed(); // 上一个spilt处理完成后继续发送切片请求 context.sendSplitRequest(); } private void reportFinishedSnapshotSplitsIfNeed() { if (!finishedUnackedSplits.isEmpty()) { final Map&lt;String, BinlogOffset&gt; finishedOffsets = new HashMap&lt;&gt;(); for (MySqlSnapshotSplit split : finishedUnackedSplits.values()) { // note: 发送切片ID,及最大偏移量 finishedOffsets.put(split.splitId(), split.getHighWatermark()); } FinishedSnapshotSplitsReportEvent reportEvent = new FinishedSnapshotSplitsReportEvent(finishedOffsets); context.sendSourceEventToCoordinator(reportEvent); LOG.debug( "The subtask {} reports offsets of finished snapshot splits {}.", subtaskId, finishedOffsets); } } MySqlSourceEnumerator 分配增量切片 全量阶段所有分片读取完毕后,MySqlHybridSplitAssigner 会创建BinlogSplit 进行后续增量读取,在创建BinlogSplit 会从全部已完成的全量切片中筛选最小BinlogOffset。注意:2.0.0分支 createBinlogSplit 最小偏移量总是从0开始,最新master分支已经修复这个BUG. private MySqlBinlogSplit createBinlogSplit() { final List&lt;MySqlSnapshotSplit&gt; assignedSnapshotSplit = snapshotSplitAssigner.getAssignedSplits().values().stream() .sorted(Comparator.comparing(MySqlSplit::splitId)) .collect(Collectors.toList()); Map&lt;String, BinlogOffset&gt; splitFinishedOffsets = snapshotSplitAssigner.getSplitFinishedOffsets(); final List&lt;FinishedSnapshotSplitInfo&gt; finishedSnapshotSplitInfos = new ArrayList&lt;&gt;(); final Map&lt;TableId, TableChanges.TableChange&gt; tableSchemas = new HashMap&lt;&gt;(); BinlogOffset minBinlogOffset = null; // note: 从所有assignedSnapshotSplit中筛选最小偏移量 for (MySqlSnapshotSplit split : assignedSnapshotSplit) { // find the min binlog offset BinlogOffset binlogOffset = splitFinishedOffsets.get(split.splitId()); if (minBinlogOffset == null || binlogOffset.compareTo(minBinlogOffset) &lt; 0) { minBinlogOffset = binlogOffset; } finishedSnapshotSplitInfos.add( new FinishedSnapshotSplitInfo( split.getTableId(), split.splitId(), split.getSplitStart(), split.getSplitEnd(), binlogOffset)); tableSchemas.putAll(split.getTableSchemas()); } final MySqlSnapshotSplit lastSnapshotSplit = assignedSnapshotSplit.get(assignedSnapshotSplit.size() - 1).asSnapshotSplit(); return new MySqlBinlogSplit( BINLOG_SPLIT_ID, lastSnapshotSplit.getSplitKeyType(), minBinlogOffset == null ? BinlogOffset.INITIAL_OFFSET : minBinlogOffset, BinlogOffset.NO_STOPPING_OFFSET, finishedSnapshotSplitInfos, tableSchemas); }

优秀的个人博客,低调大师

技术系列】浅谈GPU虚拟化技术(第一章)

第一章 GPU虚拟化发展史 GPU的虚拟化发展历程事实上与公有云市场和云计算应用场景的普及息息相关。如果在10年前谈起云计算,大部分人的反应是“不知所云“。但是随着云计算场景的普及,概念的深入人心,慢慢地大家都对云计算有一个较清晰的概念和实例化的理解。自然,随着应用场景从单一依赖CPU的计算单元的应用扩展到多种体系架构,异构计算场景的应用上来后,对GPU,FPGA,TPU等专业计算芯片也提出了虚拟化和上云的强烈要求。尤其是最近几年机器学习、深度学习等领域的快速发展,催生了异构计算场景搬迁上云的高潮。 那么这个异构计算应用场景的市场规模有多大呢?异构计算作为机器学习人工智能的计算载体,先来看看人工智能前景如何?(引用出处:https://bg.qianzhan.com/report/detail/459/180116-3c060b52

优秀的个人博客,低调大师

飞码LowCode前端技术之画布的设计 | 京东云技术团队

简介 本章节从精准定位、分层设计、异步组件、拖拽四个方面分析飞码画布设计。 一、精准定位设计 飞码画布是一个套件,可对外提供画布能力。精准定位有两种情况,一是目标组件无子组件,而是目标组件有子组件。 无子组件:目标组件分为支持与不支持放子组件两种情况。 有子组件:鼠标相对于子组件(目标组件)对角线位置。详见图1 图1 当目标组件不支持放子组件时,需要计算拖拽组件放在目标组件的左侧、上侧、右侧、还是下侧?其计算方法如图2 图2 通过鼠标位置,目标组件,组件对角线坐标位置可推导出图1右侧图拖拽组件与目标组件位置关系。 问题:飞码为何不提供尺度(x、y),这样可以精准知道组件大小? 实际使用过程中,搭建人员并不关心组件的具体x,y。一般关注一行几列与组件宽度。 二、分层设计 低代码画布设计有很多方案,飞码采用的是双层设计模式。该设计模式优势很多,与画布中组件是解耦关系。开发过iOS,安卓native的同学较容易理解。如图3 图3 画布中底层是组件渲染层,根据页面DSL渲染组件布局,在组件渲染层上还有一层canvas-mask视图。当点击某一个组件之后,根据组件会在组件最边框添加颜色,组件右侧上方(根据页面布局自动切换到下方)添加工具条(更多、上移、下移、复制、删除),hover区域。支持组件宽度拖拽调整,组件的最右侧有一个呼吸道效果的线条,鼠标可以对该组件宽度进行拖拽调整。这样极大方便了样式调整操作。 问题:既然支持了组件左右大小调整,为何不支持组件的上下大小调整? 飞码对div,form等容器组件在编辑态中上下大小会根据子组件高度进行自动调整。飞码并不知道组件的宽度大小。 三、异步组件 飞码提供常用的组件能力,飞码搭建业务定制化的组件困难。飞码提供动态加载组件能力,动态组件加载分为编辑态与运行态。编辑态在组件拖到页面的时候会根据组件数据中type判断当前组件类型。若type=2,飞码引擎会创建script下载相关url对应的组件,之后做缓存。运行态思路一致。 四、拖拽设计 拖拽组件的时,每一个组件需要混入一些特定处理,例如form表单的子组件是不是el-form-item等情况。见图4 图4 组件拖拽开始会记录currentTarget获取到组件id,并对dataTransfer进行设置image。这样就可以看到拖拽组件的样式。混入方法使用hoc,增强组件的一个方法。详见图5所示。 图5 四:小结 本章节分析了飞码画布在精准定位、分层设计、异步组件、拖拽四个方面的设计。飞码的目标是:便捷、稳健、0测试,使前端web单页面快速投产。感谢产品同学和服务端同学的大力支持。 作者:京东科技 王光辉 来源:京东云开发者社区 转载请注明来源

优秀的个人博客,低调大师

前端技术探秘-Nodejs的CommonJS规范实现原理 | 京东物流技术团队

了解Node.js Node.js是一个基于ChromeV8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式I/O模型,让JavaScript 运行在服务端的开发平台,它让JavaScript成为与PHP、Python、Perl、Ruby等服务端语言平起平坐的脚本语言。Node中增添了很多内置的模块,提供各种各样的功能,同时也提供许多第三方模块。 模块的问题 为什么要有模块 复杂的前端项目需要做分层处理,按照功能、业务、组件拆分成模块, 模块化的项目至少有以下优点: 便于单元测试 便于同事间协作 抽离公共方法, 开发快捷 按需加载, 性能优秀 高内聚低耦合 防止变量冲突 方便代码项目维护 几种模块化规范 CMD(SeaJS 实现了 CMD) AMD(RequireJS 实现了 AMD) UMD(同时支持 AMD 和 CMD) IIFE (自执行函数) CommonJS (Node 采用了 CommonJS) ES Module 规范 (JS 官方的模块化方案) Node中的模块 Node中采用了 CommonJS 规范 实现原理: Node中会读取文件,拿到内容实现模块化, Require方法 同步引用 tips:Node中任何js文件都是一个模块,每一个文件都是模块 Node中模块类型 内置模块,属于核心模块,无需安装,在项目中不需要相对路径引用, Node自身提供。 文件模块,程序员自己书写的js文件模块。 第三方模块, 需要安装, 安装之后不用加路径。 Node中内置模块 fs filesystem 操作文件都需要用到这个模块 const path = require('path'); // 处理路径 const fs = require('fs'); // file system // // 同步读取 let content = fs.readFileSync(path.resolve(__dirname, 'test.js'), 'utf8'); console.log(content); let exists = fs.existsSync(path.resolve(__dirname, 'test1.js')); console.log(exists); path 路径处理 const path = require('path'); // 处理路径 // join / resolve 用的时候可以混用 console.log(path.join('a', 'b', 'c', '..', '/')) // 根据已经有的路径来解析绝对路径, 可以用他来解析配置文件 console.log(path.resolve('a', 'b', '/')); // resolve 不支持/ 会解析成根路径 console.log(path.join(__dirname, 'a')) console.log(path.extname('1.js')) console.log(path.dirname(__dirname)); // 解析父目录 vm 运行代码 字符串如何能变成 JS 执行呢? 1.eval eval中的代码执行时的作用域为当前作用域。它可以访问到函数中的局部变量。 let test = 'global scope' global.test1 = '123' function b(){ test = 'fn scope' eval('console.log(test)'); //local scope new Function('console.log(test1)')() // 123 new Function('console.log(test)')() //global scope } b() 2.new Function new Function()创建函数时,不是引用当前的词法环境,而是引用全局环境,Function中的表达式使用的变量要么是传入的参数要么是全局的值 Function可以获取全局变量,所以它还是可能会有变量污染的情况出现 function getFn() { let value = "test" let fn = new Function('console.log(value)') return fn } getFn()() global.a = 100 // 挂在到全局对象global上 new Function("console.log(a)")() // 100 3.vm 前面两种方式,我们一直强调一个概念,那就是变量的污染 VM的特点就是不受环境的影响,也可以说他就是一个沙箱环境 在Node中全局变量是在多个模块下共享的,所以尽量不要在global中定义属性 所以,vm.runInThisContext可以访问到global上的全局变量,但是访问不到自定义的变量。而vm.runInNewContext访问不到global,也访问不到自定义变量,他存在于一个全新的执行上下文 const vm = require('vm') global.a = 1 // vm.runInThisContext("console.log(a)") vm.runInThisContext("a = 100") // 沙箱,独立的环境 console.log(a) // 1 vm.runInNewContext('console.log(a)') console.log(a) // a is not defined Node模块化的实现 node中是自带模块化机制的,每个文件就是一个单独的模块,并且它遵循的是CommonJS规范,也就是使用require的方式导入模块,通过module.export的方式导出模块。 node模块的运行机制也很简单,其实就是在每一个模块外层包裹了一层函数,有了函数的包裹就可以实现代码间的作用域隔离。 我们先在一个js文件中直接打印arguments,得到的结果如下图所示,我们先记住这些参数。 console.log(arguments) // exports, require, module, __filename, __dirname Node中通过modules.export 导出,require 引入。其中require依赖node中的fs模块来加载模块文件,通过fs.readFile读取到的是一个字符串。 在javascrpt中可以通过eval或者new Function的方式来将一个字符串转换成js代码来运行。但是前面提到过,他们都有一个致命的问题,就是变量的污染。 实现require模块加载器 首先导入依赖的模块path,fs,vm, 并且创建一个Require函数,这个函数接收一个modulePath参数,表示要导入的文件路径 const path = require('path'); const fs = require('fs'); const vm = require('vm'); // 定义导入类,参数为模块路径 function Require(modulePath) { ... } 在Require中获取到模块的绝对路径,使用fs加载模块,这里读取模块内容使用new Module来抽象,使用tryModuleLoad来加载模块内容,Module和tryModuleLoad稍后实现,Require的返回值应该是模块的内容,也就是module.exports。 // 定义导入类,参数为模块路径 function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 创建模块,新建Module实例 const module = new Module(absPathname); // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports; } Module的实现就是给模块创建一个exports对象,tryModuleLoad执行的时候将内容加入到exports中,id就是模块的绝对路径。 // 定义模块, 添加文件id标识和exports属性 function Module(id) { this.id = id; // 读取到的文件内容会放在exports中 this.exports = {}; } node模块是运行在一个函数中,这里给Module挂载静态属性wrapper,里面定义一下这个函数的字符串,wrapper是一个数组,数组的第一个元素就是函数的参数部分,其中有exports,module,Require,__dirname,__filename, 都是模块中常用的全局变量. 第二个参数就是函数的结束部分。两部分都是字符串,使用的时候将他们包裹在模块的字符串外部就可以了。 // 定义包裹模块内容的函数 Module.wrapper = [ "(function(exports, module, Require, __dirname, __filename) {", "})" ] _extensions用于针对不同的模块扩展名使用不同的加载方式,比如JSON和javascript加载方式肯定是不同的。JSON使用JSON.parse来运行。 javascript使用vm.runInThisContext来运行,可以看到fs.readFileSync传入的是module.id也就是Module定义时候id存储的是模块的绝对路径,读取到的content是一个字符串,使用Module.wrapper来包裹一下就相当于在这个模块外部又包裹了一个函数,也就实现了私有作用域。 使用call来执行fn函数,第一个参数改变运行的this传入module.exports,后面的参数就是函数外面包裹参数exports, module, Require, __dirname, __filename。/ // 定义扩展名,不同的扩展名,加载方式不同,实现js和json Module._extensions = { '.js'(module) { const content = fs.readFileSync(module.id, 'utf8'); const fnStr = Module.wrapper[0] + content + Module.wrapper[1]; const fn = vm.runInThisContext(fnStr); fn.call(module.exports, module.exports, module, Require,__filename,__dirname); }, '.json'(module) { const json = fs.readFileSync(module.id, 'utf8'); module.exports = JSON.parse(json); // 把文件的结果放在exports属性上 } } tryModuleLoad函数接收的是模块对象,通过path.extname来获取模块的后缀名,然后使用Module._extensions来加载模块。 // 定义模块加载方法 function tryModuleLoad(module) { // 获取扩展名 const extension = path.extname(module.id); // 通过后缀加载当前模块 Module._extensions[extension](module); // 策略模式??? } 到此Require加载机制基本就写完了。Require加载模块的时候传入模块名称,在Require方法中使用path.resolve(__dirname, modulePath)获取到文件的绝对路径。然后通过new Module实例化的方式创建module对象,将模块的绝对路径存储在module的id属性中,在module中创建exports属性为一个json对象。 使用tryModuleLoad方法去加载模块,tryModuleLoad中使用path.extname获取到文件的扩展名,然后根据扩展名来执行对应的模块加载机制。 最终将加载到的模块挂载module.exports中。tryModuleLoad执行完毕之后module.exports已经存在了,直接返回就可以了。 接下来,我们给模块添加缓存。就是文件加载的时候将文件放入缓存中,再去加载模块时先看缓存中是否存在,如果存在直接使用,如果不存在再去重新加载,加载之后再放入缓存。 // 定义导入类,参数为模块路径 function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 从缓存中读取,如果存在,直接返回结果 if (Module._cache[absPathname]) { return Module._cache[absPathname].exports; } // 创建模块,新建Module实例 const module = new Module(absPathname); // 添加缓存 Module._cache[absPathname] = module; // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports; } 增加功能:省略模块后缀名。 自动给模块添加后缀名,实现省略后缀名加载模块,其实也就是如果文件没有后缀名的时候遍历一下所有的后缀名看一下文件是否存在。 // 定义导入类,参数为模块路径 function Require(modulePath) { // 获取当前要加载的绝对路径 let absPathname = path.resolve(__dirname, modulePath); // 获取所有后缀名 const extNames = Object.keys(Module._extensions); let index = 0; // 存储原始文件路径 const oldPath = absPathname; function findExt(absPathname) { if (index === extNames.length) { return throw new Error('文件不存在'); } try { fs.accessSync(absPathname); return absPathname; } catch(e) { const ext = extNames[index++]; findExt(oldPath + ext); } } // 递归追加后缀名,判断文件是否存在 absPathname = findExt(absPathname); // 从缓存中读取,如果存在,直接返回结果 if (Module._cache[absPathname]) { return Module._cache[absPathname].exports; } // 创建模块,新建Module实例 const module = new Module(absPathname); // 添加缓存 Module._cache[absPathname] = module; // 加载当前模块 tryModuleLoad(module); // 返回exports对象 return module.exports; } 源代码调试 我们可以通过VSCode 调试Node.js 步骤 创建文件a.js module.exports = 'abc' 1.文件test.js let r = require('./a') console.log(r) 1.配置debug,本质是配置.vscode/launch.json文件,而这个文件的本质是能提供多个启动命令入口选择。 一些常见参数如下: program控制启动文件的路径(即入口文件) name下拉菜单中显示的名称(该命令对应的入口名称) request分为 launch(启动)和 attach(附加)(进程已经启动) skipFiles指定单步调试跳过的代码 runtimeExecutable设置运行时可执行文件,默认是 node,可以设置成 nodemon,ts-node,npm 等 修改launch.json,skipFiles指定单步调试跳过的代码 将test.js 文件中的require方法所在行前面打断点 执行调试,进入源码相关入口方法 梳理代码步骤 1.首先进入到进入到require方法:Module.prototype.require 2.调试到Module._load 方法中,该方法返回module.exports,Module._resolveFilename方法返回处理之后的文件地址,将文件改为绝对地址,同时如果文件没有后缀就加上文件后缀。 3.这里定义了Module类。id为文件名。此类中定义了exports属性 4.接着调试到module.load 方法,该方法中使用了策略模式,Module._extensions[extension](this, filename)根据传入的文件后缀名不同调用不同的方法 5.进入到该方法中,看到了核心代码,读取传入的文件地址参数,拿到该文件中的字符串内容,执行module._compile 6.此方法中执行wrapSafe方法。将字符串前后添加函数前后缀,并用Node中的vm模块中的runInthisContext方法执行字符串,便直接执行到了传入文件中的console.log代码行内容。 至此,整个Node中实现require方法的整个流程代码已经调试完毕,通过对源代码的调试,可以帮助我们学习其实现思路,代码风格及规范,有助于帮助我们实现工具库,提升我们的代码思路,同时我们知道相关原理,也对我们解决日常开发工作中遇到的问题提供帮助。 作者:京东物流 乔盼盼 来源:京东云开发者社区 自猿其说Tech 转载请注明来源

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册