首页 文章 精选 留言 我的

精选列表

搜索[超融合软件],共10000篇文章
优秀的个人博客,低调大师

核电/核工业融合调度通信系统整体解决方案

核电、核工业属于国家战略高危特种行业,厂区涵盖核岛、常规岛、BOP辅助厂房、放射性废物处理区、仓储库区、室外管线廊道、应急指挥中心、办公管控区等多类特殊场景,具备安全等级极高、电磁环境复杂、辐射区域特殊、设备抗震防振要求严苛、24小时不间断连续生产、核事故零容忍的行业特征。核电厂生产运行、设备巡检、工艺操作、故障处置、核应急响应、安保防控、检修施工全流程对通信系统的实时性、可靠性、冗余性、抗干扰性、可追溯性、防爆防辐射性有着远超普通工业的严苛标准。

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

Proxy-Go v15.3 发布,强化融合代理 + 内网穿透!

Proxy 是 golang 实现的高性能 http、https、websocket、tcp、udp、socks5 代理服务器,支持正向代理、反向代理、透明代理、内网穿透、TCP/UDP 端口映射、SSH 中转、TLS 加密传输、协议转换、DNS 防污染智能代理、前置 CDN/Nginx 反代、代理连接重定向、API 动态调用上级代理、限速限连接数。提供全平台的命令行版本,友好易用的 Windows&Linux&macOS 控制面板,强大的安卓版。 更新内容 1、内网穿透bridge新增独立HTTP认证API,覆盖client/server的session与stream四类事件;该功能不复用、不修改HTTP/SOCKS5/SPS的BasicAuth。 2、bridge新增--server-keys白名单;普通server和SPS统一使用--server-key向bridge传递server key,不增加其它同义参数。 3、bridge认证API携带双方key、远端/本地地址、目标、server/session/stream关联ID、SPS标记、bridge运行信息及独立machine_id;支持超时、重试、成功/失败缓存、认证日志和认证代理。 4、bridge新增认证API --auth-url 相关参数。启用server key白名单或认证API时,bridge、server和SPS要求版本不低于v15.4;client协议不变,旧client仍兼容。 5、未启用bridge认证且不传--server-key时保持旧协议,新增新旧bridge/client/server/SPS双向混合黑盒测试。 6、优化--forever逻辑,对于认证失败的情况,限制重试次数,避免过多无效重启。 7、SPS的API认证返回的upstream新增bridge路由,支持通过TCP或WS连接bridge,并按client key把HTTP(S)、SOCKS5的TCP流量交给对应client直连目标。 8、SPS新增--session-count参数,默认10,用于设置每个bridge接入端点的独立session池容量;停止SPS时会完整释放池内session和关联连接。 9、原有http(s)、socks5、ss upstream格式和内网穿透client使用方式保持兼容。bridge upstream功能要求SPS和bridge版本不低于v15.3,详细格式见中英文手册。 10、修复HTTP(S)/SOCKS5/SPS的--auth-timeout参数未实际应用到认证API请求的问题。 更新: v10.7 及以后版本,执行:proxy update,即可完成快速更新到最新版。 本次更新的重点,是让 SPS 的认证 API 可以直接返回一个内网穿透 bridge upstream。SPS 收到代理请求后,不再只能连接传统的 HTTP、SOCKS5 或 SS 上级,而是可以根据用户、来源地址或访问目标,动态选择一个内网穿透 client,由该 client 直连目标。 这使同一个 SPS 可以按请求把流量送到不同的内网环境,同时保留现有的代理协议入口和认证策略。 特色功能 链式代理,程序本身可以作为一级代理,如果设置了上级代理那么可以作为二级代理,乃至 N 级代理。 通讯加密,如果程序不是一级代理,而且上级代理也是本程序,那么可以加密和上级代理之间的通讯,采用底层 tls 高强度加密,安全无特征。 智能 HTTP 代理,HTTPS 代理,SOCKS5 代理,会自动判断访问的网站是否屏蔽,如果被屏蔽那么就会使用上级代理 (前提是配置了上级代理) 访问网站;如果访问的网站没有被屏蔽,为了加速访问,代理会直接访问网站,不使用上级代理。 域名黑白名单,更加自由的控制网站的访问方式。 跨平台性,无论你是 widows,linux,还是 mac,甚至是树莓派,都可以很好的运行 proxy。 多协议支持,支持 HTTP (S),TCP,UDP,Websocket,SOCKS5 代理。 TCP/UDP 端口转发。 游戏盾,游戏代理,高仿服务器。 内网穿透,P2P 传输,协议支持 TCP 和 UDP,针对 HTTP 的优化穿透。 SSH 中转,HTTP (S),SOCKS5 代理支持 SSH 中转,上级 Linux 服务器不需要任何服务端,本地一个 proxy 即可开心上网。 KCP协议支持,HTTP (S),SOCKS5 代理支持 KCP 协议传输数据,降低延迟,提升浏览体验。 动态选择上级代理,通过外部 API,HTTP (S),SOCKS5,SPS 代理可以实现基于用户或者 IP 的限速,连接数限制,动态获取上级。 灵活的上级分配,HTTP (S),SOCKS5,SPS 代理可以通过配置文件实现基于用户或者 IP 的限速,连接数限制,指定上级。 反向代理,支持直接把域名解析到 proxy 监听的 ip,然后 proxy 就会帮你代理访问需要访问的 HTTP (S) 网站。 透明 HTTP (S) 代理,配合 iptables,在网关直接把出去的 80,443 方向的流量转发到 proxy,就能实现无感知的智能路由器代理。 协议转换,可以把已经存在的 HTTP (S) 或 SOCKS5 或 SS 代理转换为一个端口同时支持 HTTP (S) 和 SOCKS5 和 SS 代理,转换后的 SOCKS5 和 SS 代理如果上级是 SOCKS5 代理,那么支持 UDP 功能,同时支持强大的级联认证功能。 自定义底层加密传输,http (s)\sps\socks 代理在 tcp 之上可以通过 tls 标准加密以及 kcp 协议加密 tcp 数据,除此之外还支持在 tls 和 kcp 之后进行自定义加密,也就是说自定义加密和 tls|kcp 是可以联合使用的,内部采用 AES256 加密,使用的时候只需要自己定义一个密码即可。 底层压缩高效传输,http (s)\sps\socks 代理在 tcp 之上可以通过自定义加密和 tls 标准加密以及 kcp 协议加密 tcp 数据,在加密之后还可以对数据进行压缩,也就是说压缩功能和自定义加密和 tls|kcp 是可以联合使用的。 安全的 DNS 代理,可以通过本地的 proxy 提供的 DNS 代理服务器与上级代理加密通讯实现安全防污染的 DNS 查询。 负载均衡,高可用,HTTP (S)\SOCKS5\SPS 代理支持上级负载均衡和高可用,多个上级重复 - P 参数即可。 指定出口 IP,HTTP (S)\SOCKS5\SPS\TCP 代理支持客户端用入口 IP 连接过来的,就用入口 IP 作为出口 IP 访问目标网站的功能。如果入口 IP 是内网 IP,出口 IP 不会使用入口 IP 支持限速,HTTP (S)\SOCKS5\SPS\TCP 代理支持限速。 支持限连接数,HTTP (S)\SOCKS5\SPS\TCP 代理支持限连接数。 SOCKS5 代理支持级联认证。 证书参数使用 base64 数据,默认情况下 - C,-K 参数是 crt 证书和 key 文件的路径,如果是 base64:// 开头,那么就认为后面的数据是 base64 编码的,会解码后使用。 支持客户端 IP 黑白名单,更加安全的控制客户端对代理服务的访问,如果黑白名单同时设置,那么只有白名单生效。socks/http (s)/sps/tcp/udp/dns/ 内网穿透 bridge / 内网穿透 tbridge,都支持客户端 IP 黑白名单。 端口范围批量监听,HTTP (S)\SOCKS5\SPS\TCP 代理支持指定端口范围监听,避免启动过多进程,提高性能。 下载地址: Gitee Github

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

融合 JPA 和超强查询的 sqltoy-orm 5.6.7 发版

开源地址: github:https://github.com/sagframe/sagacity-sqltoy gitee:https://gitee.com/sagacity/sagacity-sqltoy idea 插件 (可直接在 idea 中检索安装):https://github.com/imyuyu/sqltoy-idea-plugin sqltoy 脚手架项目:https://gitee.com/momoljw/sss-rbac-admin sqltoy lambda 项目:https://gitee.com/gzghde/sqltoy-plus 更新内容 1、修复ToChar函数:场景to_char(t.total_amt)单参数场景2、打springboot-starter包时增加yml配置提示json到META-INFO下3、BeanUtil、CollectionUtil部分代码编写优化4、修复oracle timestamp转localDateTime精度问题 JPA 部分 类似 JPA 的对象化 CRUD、对象级联加载和新增、更新 支持通过 POJO 生成 DDL 以及直接向数据库创建表 强化 update 操作,提供弹性字段修改能力,不同于 hibernate 先 load 后修改,而是一次数据库交互完成修改,确保了高并发场景下数据的准确性 改进了级联修改,提供了先删除或者先置无效,再覆盖的操作选项 增加了 updateFetch、updateSaveFetch 功能,强化针对强事务高并发场景的处理,类似库存台账、资金台账,实现一次数据库交互,完成锁查询、不存在则插入、存在则修改,并返回修改后的结果 增加了树结构封装,便于统一不同数据库树型结构数据的递归查询 支持分库分表、支持多种主键策略 (额外支持基于 redis 的产生特定规则的业务主键)、加密存储、数据版本校验 提供了公共属性赋值 (创建人、修改人、创建时间、修改时间、租户)、扩展类型处理等 提供了多租户统一过滤和赋值、提供了数据权限参数带入和越权校验 查询部分 极为直观的 sql 编写方式,便于从客户端 <--> 代码 双向快速迁移,便于后期变更维护 支持缓存翻译、反向缓存匹配 key 代替 like 模糊查询 提供了跨数据库支持能力:不同数据库的函数自动转换适配,多方言 sql 根据实际环境自动匹配、多数据库同步测试,大幅提升了产品化能力 提供了取 top 记录、随机记录等特殊场景的查询功能 提供了最强大的分页查询机制:1) 自动优化 count 语句;2) 提供基于缓存的分页优化,避免每次都执行 count 查询;3) 提供了独具特色的快速分页;4) 提供了并行分页 提供了分库分表能力 提供了在管理类项目中极为价值的:分组汇总计算、行列转换 (行转列、列转行)、同比环比、树形排序、树形汇总 相关算法自然集成 提供了基于查询的层次化数据结构封装 提供了大量辅助功能:数据脱敏、格式化、条件参数预处理等 支持多种数据库 常规的 mysql、oracle、db2、postgresql、 sqlserver、dm、kingbase、sqlite、h2、 oceanBase、polardb、guassdb、tidb 支持分布式 olap 数据库: clickhouse、StarRocks、greenplum、impala (kudu) 支持 elasticsearch、mongodb 所有基于 sql 和 jdbc 各类数据库查询 sqltoy 特点介绍: sqltoy 的核心构建思想 sqltoy 的对比 mybatis (plus) 的核心点:查询语句编写、可阅读性、可维护性 对象化 crud 是基础,但 sqltoy 有针对性的改进:update、updateSaveFetch、updateFetch 等 sqltoy 的缓存翻译,大幅减少表关联简化 sql,让你的查询性能成几何级提升 极致的分页,同样帮助你实现查询的性能大幅提升 快速分页:@fast () 实现先取单页数据然后再关联查询,极大提升速度 分页优化器:page-optimize 让分页查询由两次变成 1.3~1.5 次 (用缓存实现相同查询条件的总记录数量在一定周期内无需重复查询 sqltoy 的分页取总记录的过程不是简单的 select count (1) from (原始 sql);而是智能判断是否变成:select count (1) from 'from 后语句 ', 并自动剔除最外层的 order by sqltoy 支持并行查询:parallel="true",同时查询总记录数和单页数据,大幅提升性能 便利的跨数据库统计计算:数据旋转 便利的跨数据库统计计算:无限极分组统计 (含汇总求平均) 便利的跨数据库统计计算:同比环比 5、树形表排序汇总 6、扩展集成

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

【核心技术】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); }

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

每日一博 | 京东 app 后台多端融合架构代码重构实战

一 简介 重构是一个非常常见且古老的课题,涉及重构的文章、书更是不可胜数。 但其实做程序做久了就会知道,想把一个复杂的系统做好,尤其是参与人数较多的中大型项目,靠看几本设计模式的书,去试图寻找设计模式的奥秘,其实是不够的。很多时候,看书时觉得很有道理,例子也能理解,但到实际开发时,却无从下手,不知道怎么灵活套用。 很多项目,在持续的版本迭代中,还伴随着人员的更替过程,往往为了解决眼前的需求,最常见的就是直接复制类似的逻辑,或者就是在末尾追加逻辑。同时,受限于对老版本的需求理解,很容易出现新需求覆盖老需求的参数值,无意识的更改老版本结果等。那么倘若系统的隔离性做的不好,则极易产生A功能的改动,影响BCD等一大堆功能的正确性。 以京东App的后台为例,就是一个典型的复杂系统,涉及开发人员众多,模块巨多,迭代时间很长,很多业务逻辑已无从考证,开发人员也已经换了好几轮,那么对于这样的系统,如何让开发人员做的需求、功能隔离开,互不影响,各小模块又能各自健壮、系统又具备相当的扩展性、配置化率又高(仅通过配置即可完成功能的变更),就成了一个不得不深入考虑的问题。 本篇主要是我在实际工作中,对一个复杂系统做了一些改造,并从中总结的一些经验,做的分享。 二 案例 近期京东App后台核心模块发生了较大的逻辑改动,主要原因是新增了一些接入来源,从之前的独苗京东App,到后来的京东PC站、京东极速版、老年版、小程序等等,都接入了原App后台。通过完成统一接入,避免了多个后台共存,重复开发的问题。 而这些不同的来源,逻辑就有较多不同。 有的来源需要执行逻辑ABC,有的来源需要执行BCD,有的只需要执行BC,并且不同的来源返回值也有所不同,这就对之前的单一来源的系统架构产生了较大的冲击,如果处理不当,则不可避免地出现大量的if-else逻辑,以及扩展上的混乱。 那么针对这种情况,以及对提高系统整体配置化率的诉求,我们对后台架构做了一次重构。本文就是对重构内容做的一个浓缩后的抽象讲解,线上实战性质,非单纯设计模式类的demo。 如下图,我尽可能简化了细节,和小模块内的逻辑,仅保留了最外层的大模块。 我们来看背景,之前只有主App客户端来源的请求,譬如refer=1,该请求到达后,需要触发"运费"、"优惠券"等数十个上游rpc调用,之后聚合各上游系统结果,返回给客户端对应结果。 现在新增的接入方,譬如"老年版",refer=5,就删减了组件层一些复杂的促销逻辑、凑单、白条之类,结果层也有相应删改。我们该如何支撑这种可能随时增删改模块和来源的业务架构呢? 三 原始问题点——复杂的排列组合 当多个层级均出现了多个变量时,这个系统的逻辑就变成了一个复杂的排列组合问题。 我们假如用户的入参是User对象,里面有一个字段refer表明了来源。 在重构前,代码进入主流程后,如下图,就是简单地根据refer来决定是否走哪些模块、返回哪些参数。 图中仅作为示例,实际情况,每个 fetch模块都有数千数万行代码,逻辑之复杂,各种if、else运用之嵌套,各种与或非使用之犀利,实属鲁班再世,也要夸赞几句鬼斧神工的。 大家都能看出来的问题,就是业务模块与入参变量的强耦合,如果入参refer=1,则执行业务模块A、B、C,不执行E、F、G,且返回值包含X、Y、Z,不包含U、V、W。 这样的设计不可避免带来了极大的维护的麻烦与混乱,到处都在判断是否是它,是他还是她? 那么该如何隔离层级,解耦模块与来源、来源与返回值之间的关联呢。 四 简化判断,让职责单一 上面提到了最大的问题,我们用通俗的话来讲就是:如果是A,我就做A1、A2、A3;如果是B,我就做A2,A3,A4。 那么问题就是这个主逻辑器做了太多的事情,日后被修改的概率极大,每个逻辑变动,都会导致主逻辑器的改动。 我们主要优化的点就是将这个逻辑给去掉,让主逻辑器职责单一,每个业务单元也职责单一。将上面的逻辑变成如果是A1,则来源是A时我工作,是B时我不工作,如果是A2,则来源A、B我都工作,如果是A4,则来源A我不工作,来源B我工作。 可以看到,做的事情就是当有一堆条件判断,要决定执行N个逻辑中的M个时,调用者不应该关心调用逻辑,而应由这N个逻辑自行判断自己要不要执行。 从代码实现来看,就是调用者不关心有多少个逻辑块,也不必关心日后的增减,从而实现调用的解耦。那么代码该如何写呢? 原来的是在一个方法里,fetchStock,fetchDiscount等等,首先我们要把这些实现全部去除,并统一为对接口的遍历。 改造后的代码是这样 代码很简单,注入一个接口的集合,并遍历这个集合,根据实现类返回的true、false决定是否要执行这个实现类的业务逻辑。 接口定义如下 单个逻辑单元代码如下 以上主流程的逻辑很清晰,后续随着各模块的扩展或缩减,都不需要动主逻辑,而只需要各个子模块根据自己的情况返回是否要执行自己即可。 五 动态配置,避免硬编码 通过以上的改造,我们已经完成了模块间的隔离,当有新增、删减模块时,可以做到不影响主流程,且将代码修改、影响范围控制在一个类里。 但是需求的变化总是很频繁,仅仅做到互不影响还不满足需求,我们还需要做到能够动态的控制各个模块的启用和关闭。 譬如『如果是A,我就做A1、A2、A3;如果是B,我就做A2,A3,A4』。希望能做到随时仅通过修改配置,不改代码不重新发布而做到『如果是A,我就做A1、A2;如果是B,我就做A3,A4』,完成对模块的启停。 动态配置该如何实现呢? 其实很简单,我们只需要修改execute方法,将refer==1这种规则存放于配置中心,将execute方法里的硬编码判断变成根据配置中心的配置进行判断即可。如下图: 那么ConfigCenter就是配置中心工具类,里面提供了根据key获取value的方法。配置中心大抵如注释所描述的,在应用启动时,全量从zk、etcd等拉取配置并保存在本地内存,并开启监听,当配置中心内容有变化时,更新到本地内存里。 通过观察各个模块的execute类,可以进一步发现,倘若配置中心里我们将类名作为key,模块所支持的refer集合为value时,各模块的execute方法就是完全一样的代码。那么整个方法又可以进一步抽成一个抽象类,由该抽象类来完成这个判断逻辑,如图: 当有了统一完成"开关"的父类后,则各个模块的逻辑单元就更加简单了,只需要关注自己的业务逻辑即可。 至此,最小业务单元职责则回归到纯粹的业务逻辑,不再参与流程控制的逻辑判断。同理,主流程也不再参与对各个子业务单元的判断和控制,只关注于对接口的遍历,各模块也不再产生相互影响。 六 结果层字段隔离控制 以上我们完成了业务逻辑单元的隔离,那么对于结果层该如果控制呢? 原始代码是这样的,在主流程中对各个变量进行判断,然后设置结果的值。从原始代码可以看到,即便只有一个变量refer就已经让代码可维护性变的很差,更别提真实场景下变量可能有多个时,要维护不同变量场景下返回不同的参数该多少困难。 通过对上面业务层控制的实现,我们同样可以采用类似的方式来处理结果层。 定义一个接口如下,定义boolean型方法,让各字段决定自己要不要返回。定义key、value,用来存放字段名和value。 修改主流程如下,当需要返回时,才能待返回字段的key、value存起来。 实现类如下: 以上方式展示了对结果层进行精细化控制的简单方案,实际场景中,可能涉及结果层数据结构并不是单层、对key、value的判断需要额外的属性等,其实思路都是一样的。 如果要增加入参的判断,在接口的needOut里追加要参加逻辑判断的入参即可。如果返回的结构不是单层的key-value,则在复合结构的实现类里再嵌套一层新定义的接口的遍历也可。 七 长逻辑相关处理 长逻辑这种最常见,也是最好处理的。我们经常在写一处逻辑时,刚开始很简单,几行就解决了,后来随着业务越来越膨胀,这个方法也是越来越长。终于有一天,代码长到显示器装不下它了,后面的逻辑开始出现对前面的逻辑产生影响了,这个方法就开始变的有"坏味道"。 这种相信大家都不少见,尤其在老系统中,从1千到8千行的我都见过,编辑器右边的滚动条都要看不到了。当然仅仅是长倒还好说,主要的问题是相互影响,前面赋的值,后面就被覆盖了,这种问题往往还比较隐蔽,极其影响系统的健康。 解决这种长逻辑,其实很简单,做好两件事即可,1-将方法的顺序执行变成接口遍历,2-封装。 1 如何将方法的顺序执行变成遍历呢? 这个其实在上面已经讲了,是类似的做法。将一大堆在同一个大方法里的小方法全部变成某个接口的实现类,从而将方法的顺序执行,变成对接口集合的遍历执行。后续增加或删减方法时,只操作对应的类即可,而不需要对这个大流程做修改。 一个单独的实现类就是这样的: 可能有人会问,我的各个方法是有先后顺序的,你用了接口集合,该怎么控制顺序呢,从上图的Order注解可以看到,这个就是控制在接口实现类的顺序的,值越小,在List里越靠前。 2 如何理解封装呢? 这个更简单,之前不是说代码长了易出现值被后面的逻辑覆盖,那么就以某个最小参数为一个类,所有对他的增删改都控制在一个类,完成对某参数、对象的封装控制。而不要散落各地去修改一个参数的值。 八 带中断的长逻辑处理 最后一个问题,如果带有流程中断的情况。如图,一个长逻辑,在某些条件被中断了,中断后后面的逻辑自然是走不到了。那么之前的对接口集合遍历方式还能用吗? 自然是可以用的,不过就要稍加改造,让实现类的方法返回一个boolean值,当false时,中断这个循环流程即可,这样后面的逻辑就走不到了。 如果不是要中断,而是某条件下执行自己,某条件下跳过自己,这个就往上看看文章的第4段。 最后 本文通过一些例子,描述了一些场景下对系统的改造方式,由于京东APP后台逻辑复杂,以上场景仅覆盖了部分典型场景,未全部写出改造点,当然还有一部分是特有的非典型问题,可能大部分用户碰不到的场景,也未写出。 如果有遇到类似场景,可参考文中的一些方式进行处理。如果有问题,或有建议,亦可联系作者wuweifeng10@jd.com。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

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

Rocky Linux

Rocky Linux

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

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册