Flink实时数仓同步:流水表实战详解

一、背景

在大数据领域,初始阶段业务数据通常被存储于关系型数据库,如MySQL。然而,为满足日常分析和报表等需求,大数据平台采用多种同步方式,以适应这些业务数据的不同存储需求。这些同步存储方式包括离线仓库和实时仓库等,选择取决于业务需求和数据特性。

一项常见需求是,大数据分析平台需要能够检索某张业务表的变更记录,并以每天为单位统计每条数据的变更频率。以下是示例:

  1. [Mysql] 业务数据 - 用户表全量数据:
idnamephonegendercreate_timeupdate_time
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom3332023-06-01 13:00:002023-06-01 13:00:00
  1. [Mysql] 2023-06-02 业务数据新增了一名tony用户,且多次更改了tom的手机号,此时表数据如下:
idnamephonegendercreate_timeupdate_time备注
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom5552023-06-01 13:00:002023-06-02 09:00:00(手机号从333->444->555)
4tony6662023-06-02 10:00:002023-06-02 10:00:00(新增tony用户)

加粗为更新/新增数据

  1. [大数据平台] 2023-06-02日业务人员在大数据分析平台中查看用户表实时变更记录,数据如下:
idnamephonegendercreate_timeupdate_timeop(操作类型)before(变更前数据)dt
3tom4442023-06-01 13:00:002023-06-02 08:00:00u{3,tom,333,男,…}2023-06-02
3tom5552023-06-01 13:00:002023-06-02 09:00:00u{3,tom,444,男,…}2023-06-02
4tony6662023-06-02 10:00:002023-06-02 10:00:00cnull2023-06-02

op字段为每条数据的操作类型:r/c/u/d

  1. [大数据平台] 业务人员在大数据分析平台中根据id+姓名维度统计2023-06-02日的变更次数,期望数据如下:
idnamecountdt
3tom22023-06-02
4tony12023-06-02

根据上述需求,我们可以得出需要构建实时流水表以满足业务数据的实时分析需求。尽管离线数仓中存在流水表,但由于以下原因,离线数仓并不符合此需求:

  1. 同步延迟问题: 离线数仓的同步方式通常为T+1,而上述需求要求实时查看当天业务数据的变更情况。
  2. 同步数据源问题: 离线数仓的T+1同步通常是通过 SQL 读取业务表数据,而不是读取 binlog 数据。这导致离线同步只能获取到 Tom 的最后一次更改数据,例如手机号=555,而无法获取所有更改项。

下面将讨论实现方案。

二、实时同步+拉链表实现

2.1、技术选型

鉴于业务数据通常存储在关系型数据库中,我们选择采用Flink-CDC持续读取binlog日志进行实时同步。为了保证实时数据能够高效写入下游并支持用户OLAP查询分析,这里选择了企业中常见的MMP库Doris作为实时数仓的存储层。整体架构如下图所示:

在这里插入图片描述

2.2、数据流转过程

Flink 实时同步程序将捕获到的 MySQL 数据变更事件进行实时处理。对于新增(INSERT)、修改(UPDATE)、删除(DELETE)等操作,构建流水表更新语句同步至下游Doris,数据流转过程如下所示:

在这里插入图片描述

2.3、流水表设计

  1. 实时流水表是在大数据分析场景中常用的数据结构之一,用于记录业务数据的实时变更情况。该表主要用于跟踪数据的变更历史,捕捉每一次更新、插入或删除的操作,以便实时分析和监控业务数据的动态变化。

  2. 背景需求需要实时检索某张业务表的历史变更记录,并以每天为单位统计每条数据的变更频率,因此在Doris中设计表结构时采用了Unique数据模型。建表语句如下,其中以id和update_time为unique key从而保证秒级唯一性,同时采用动态分区按天进行划分,建表语句如下:

CREATE TABLE `example_user_stream`
(`id` largeint(40) NOT NULL COMMENT '用户id',`update_time` datetime NULL COMMENT '用户更新时间',`dt` date NULL COMMENT '流水日期',`create_time` datetime NULL COMMENT '用户注册时间',`name` varchar(50) NOT NULL COMMENT '用户昵称',`phone` largeint(40) NULL COMMENT '手机号',`gender` varchar(5) NULL COMMENT '用户性别',`op` varchar(4) NOT NULL COMMENT '每条数据的操作类型:r/c/u/d',`before` STRING NULL COMMENT '变更前数据',`binlog` STRING NULL COMMENT 'binlog全量日志'
) ENGINE=OLAP
UNIQUE KEY(`id`, `update_time`, `dt`)
COMMENT '用户流水表'
PARTITION BY RANGE(dt)()
DISTRIBUTED BY HASH(id) BUCKETS 8
PROPERTIES
("dynamic_partition.enable" = "true","dynamic_partition.time_unit" = "DAY","dynamic_partition.start" = "-90","dynamic_partition.end" = "3","dynamic_partition.prefix" = "p","dynamic_partition.buckets" = "8"
);

该表利用了Doris的动态分区功能,将分区粒度设置为天级,并采取了预先建立3天分区的策略,同时设定了90天的过期时间;更多信息可参考Doris动态分区介绍

2.4、实时同步逻辑

  1. 首先,由于实时流水表同步使用Flink-cdc读取关系型数据库,flink-cdc提供了四种模式: “initial”,“earliest-offset”,“latest-offset”,“specific-offset” 和 “timestamp”。本文使用的Flink-connector-mysq是2.3版本,这里简单介绍一下这四种模式:

    • initial (默认):在第一次启动时对受监视的数据库表执行初始快照,并继续读取最新的 binlog。
    • earliest-offset:跳过快照阶段,从可读取的最早 binlog 位点开始读取
    • latest-offset:首次启动时,从不对受监视的数据库表执行快照, 连接器仅从 binlog 的结尾处开始读取,这意味着连接器只能读取在连接器启动之后的数据更改。
    • specific-offset:跳过快照阶段,从指定的 binlog 位点开始读取。位点可通过 binlog 文件名和位置指定,或者在 GTID 在集群上启用时通过 GTID 集合指定。
    • timestamp:跳过快照阶段,从指定的时间戳开始读取 binlog 事件。
  2. 由于流水表只需知晓更改过程而无需记录全量数据,故采用timestamp模式从指定的时间戳【今日零点】开始读取 binlog 事件开始读取;这里之所以没有选择earliest-offset模式是因为doris在建表时分区大于或等于今日,而binlog可能保存多天以前的数据而无法存放至doris分区中,故采用timestamp模式.

  3. 此外,由于实时流水表同步需要对 binlog 数据进行解析及判断更新操作类型,因此,Flink CDC SQL 方式的表建立不再满足我们的要求。为了更好地实现这一功能,我们需要采用 API 方式来构建解决方案,代码如下:

    import org.apache.flink.api.common.eventtime.WatermarkStrategy;
    import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
    import com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;
    import com.ververica.cdc.connectors.mysql.source.MySqlSource;public class MySqlSourceExample {public static void main(String[] args) throws Exception {MySqlSource<String> mySqlSource = MySqlSource.<String>builder().hostname("yourHostname").port(yourPort).databaseList("yourDatabaseName") // 设置捕获的数据库, 如果需要同步整个数据库,请将 tableList 设置为 ".*"..tableList("yourDatabaseName.yourTableName") // 设置捕获的表.username("yourUsername").password("yourPassword").startupOptions(StartupOptions.timestamp(1685548800000L)) // 从2023-06-01零点处读取binlog.deserializer(new JsonDebeziumDeserializationSchema()) // 将 SourceRecord 转换为 JSON 字符串.build();StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();// 设置 3s 的 checkpoint 间隔env.enableCheckpointing(3000);env.fromSource(mySqlSource, WatermarkStrategy.noWatermarks(), "MySQL Source")// 设置 source 节点的并行度为 4.setParallelism(4).print().setParallelism(1); // 设置 sink 节点并行度为 1 env.execute("Print MySQL Snapshot + Binlog");}
    }
    

    代码摘自mysql-cdc-connector官网示例

  4. 这里我们以2023-06-02日的[Mysql]业务数据为例,新增了一名tony用户,且多次更改了tom的手机号,此时表数据如下:

idnamephonegendercreate_timeupdate_time备注
1jack1112023-06-01 13:00:002023-06-01 13:00:00
2jason2222023-06-01 13:00:002023-06-01 13:00:00
3tom5552023-06-01 13:00:002023-06-02 09:00:00(手机号从333->444->555)
4tony6662023-06-02 10:00:002023-06-02 10:00:00(新增tony用户)

加粗为更新/新增数据

  1. 此时Flink应用获取到的数据如下:
# 新增tony变更数据如下
{"before": null,"after": {"id": 4,"name": "tony","phone": "666","gender": "男","create_time": "2023-06-02T02:00:00Z","update_time": "2023-06-02T02:00:00Z"},"source": {# 元数据信息忽略},"op": "c", # 操作类型"ts_ms": 1706768344113,"transaction": null
}
# tom手机号333->444变更数据如下
{"before": {"id": 3,"name": "tom","phone": "333","gender": "男","create_time": "2023-06-01T05:00:00Z","update_time": "2023-06-01T05:00:00Z"},"after": {"id": 3,"name": "tom","phone": "444","gender": "男","create_time": "2023-06-01T05:00:00Z","update_time": "2023-06-01T23:00:00Z"},"source": {# 元数据信息忽略},"op": "u", # 操作类型"ts_ms": 1706768454904,"transaction": null
}
# tom手机号444->555变更数据如下
{"before": {"id": 3,"name": "tom","phone": "444","gender": "男","create_time": "2023-06-01T05:00:00Z","update_time": "2023-06-01T23:00:00Z"},"after": {"id": 3,"name": "tom","phone": "555","gender": "男","create_time": "2023-06-01T05:00:00Z","update_time": "2023-06-02T00:00:00Z"},"source": {# 元数据信息忽略},"op": "u", # 操作类型"ts_ms": 1706768521452,"transaction": null
}
  1. 我们使用 Flink CDC MySQL 同步数据时可以获取到binlog数据中的 op 字段。op 字段是用来记录每条数据的操作类型的标志。具体的操作类型如下:

    • op=d 代表删除操作
    • op=u 代表更新操作
    • op=c 代表新增操作
    • op=r 代表全量读取,而不是来自 binlog 的增量读取
  2. 当 Flink 同步程序接收到 op=c 表示新增 Tony 的数据时,首先解析 binlog 日志,提取其中的 opbeforeafter 数据。接着,从 after 中截取 update_time 的日期值,并将这些信息拼装成 Doris 的 INSERT 语句,通过该语句将数据插入到 Doris 中。这一过程完成后,即成功记录了这条流水数据,sql语句如下:

INSERT INTO example_user_stream (id, update_time, dt, create_time, name, phone, gender, op, `before`, `binlog`)
VALUES 
(4, '2023-06-02 10:00:00', '2023-06-02', '2023-06-02 10:00:00', 'tony', 555, '男', 'c', null, '{"before":null,"after":{"id":4,"name":"tony","phone":"666","gender":"男","create_time":"2023-06-02T02:00:00Z","update_time":"2023-06-02T02:00:00Z"},"source":{"version":"1.6.4.Final","connector":"mysql","name":"mysql_binlog_source","ts_ms":1706768344000,"snapshot":"false","db":"yushu_dds","sequence":null,"table":"user","server_id":2307031958,"gtid":"71221bfd-56e8-11ee-8275-fa163e4ecceb:33719321","file":"3509-binlog.000191","pos":643757739,"row":0,"thread":null,"query":null},"op":"c","ts_ms":1706768344113,"transaction":null}');
  1. 当 Flink 同步程序接收到op=u修改tom手机号的数据时,和上述步骤一样提取binlog日志,需要注意的是此时要将before数据写入到doris数据中,sql语句如下:
# tom手机号333->444:
INSERT INTO example_user_stream (id, update_time, dt, create_time, name, phone, gender, op, `before`,`binlog`)
VALUES 
(3, '2023-06-02 08:00:00', '2023-06-02', '2023-06-02 13:00:00', 'tom', 444, '男', 'u', '{"id":3,"name":"tom","phone":"333","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-01T05:00:00Z"}', '{"before":{"id":3,"name":"tom","phone":"333","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-01T05:00:00Z"},"after":{"id":3,"name":"tom","phone":"444","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-01T23:00:00Z"},"source":{"version":"1.6.4.Final","connector":"mysql","name":"mysql_binlog_source","ts_ms":1706768454000,"snapshot":"false","db":"yushu_dds","sequence":null,"table":"user","server_id":2307031958,"gtid":"71221bfd-56e8-11ee-8275-fa163e4ecceb:33719761","file":"3509-binlog.000191","pos":692873739,"row":0,"thread":null,"query":null},"op":"u","ts_ms":1706768454904,"transaction":null}');# tom手机号444->555:
INSERT INTO example_user_stream (id, update_time, dt, create_time, name, phone, gender, op, `before`,`binlog`)
VALUES 
(3, '2023-06-02 09:00:00', '2023-06-02', '2023-06-02 13:00:00', 'tom', 555, '男', 'u', '{"id":3,"name":"tom","phone":"444","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-01T23:00:00Z"}', '{"before":{"id":3,"name":"tom","phone":"444","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-01T23:00:00Z"},"after":{"id":3,"name":"tom","phone":"555","gender":"男","create_time":"2023-06-01T05:00:00Z","update_time":"2023-06-02T00:00:00Z"},"source":{"version":"1.6.4.Final","connector":"mysql","name":"mysql_binlog_source","ts_ms":1706768521000,"snapshot":"false","db":"yushu_dds","sequence":null,"table":"user","server_id":2307031958,"gtid":"71221bfd-56e8-11ee-8275-fa163e4ecceb:33720328","file":"3509-binlog.000191","pos":806151026,"row":0,"thread":null,"query":null},"op":"u","ts_ms":1706768521452,"transaction":null}');
  1. [大数据平台] 2023-06-02日业务人员在大数据分析平台中查看用户表实时变更记录,sql查询及结果如下:
SELECT * FROM example_user_stream PARTITION p20230602;
idupdate_timedtcreate_timenamephonegenderopbeforebinlog(部分省略)
42023-06-02 10:00:002023-06-022023-06-02 10:00:00tony555cNULL{“before”:null,“after”:{…}…}
32023-06-02 08:00:002023-06-022023-06-02 13:00:00tom444u{“id”:3,“name”:“tom”,“phone”:“333”,“gender”:“男”,“create_time”:“2023-06-01T05:00:00Z”,“update_time”:“2023-06-01T05:00:00Z”}{“before”:null,“after”:{…}…}
32023-06-02 09:00:002023-06-022023-06-02 13:00:00tom555u{“id”:3,“name”:“tom”,“phone”:“444”,“gender”:“男”,“create_time”:“2023-06-01T05:00:00Z”,“update_time”:“2023-06-01T23:00:00Z”}{“before”:null,“after”:{…}…}
  1. [大数据平台] 业务人员在大数据分析平台中根据id+姓名维度统计2023-06-02日的变更次数,sql查询及结果如下:
SELECT id,name,count(*) as 'count',dt FROM example_user_stream PARTITION p20240201 group by dt,id,name;
idnamecountdt
3tom22023-06-02
4tony12023-06-02

至此流水表的实时同步逻辑结束,删除操作与前述的修改操作类似,实现方式基本一致。

三、总结

FlinkCDC与流水表的结合:

  1. 实时同步: FlinkCDC通过实时捕获数据库变更,结合Flink的处理能力,将变更逻辑应用到流水表中,实现实时同步。
  2. 数据一致性: 结合Flink的状态管理,可以确保对数据变更的精确追踪和一致性处理,避免数据丢失或不一致。
  3. 删除操作: FlinkCDC同样适用于删除操作,将删除事件同步到流水表,确保流水表记录了所有的数据变更历史。
  4. 动态分区支持: FlinkCDC与动态分区的流水表结合,可以更灵活地管理和查询历史数据,使得数据的存储和查询更为高效。

总体而言,FlinkCDC与流水表的结合为实时数据同步提供了可靠的解决方案,既能满足实时性要求,又能保证数据变更历史的完整性和一致性。

四、相关资料

  • Doris 数据模型
  • MySQL CDC Connector
  • Flink实时数仓同步:拉链表实战详解
  • 深入数仓离线数据同步:问题分析与优化措施

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/662728.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2024美赛】A题(中英文):资源可用性与性别比例Problem A: Resource Availability and Sex Ratios

【2024美赛】A题&#xff08;中英文&#xff09;&#xff1a;资源可用性与性别比例Problem A: Resource Availability and Sex Ratios 写在最前面2024美赛翻译 —— 跳转链接 中文赛题问题A&#xff1a;资源可用性与性别比例需要检查的问题包括&#xff1a; 英文赛题Problem A:…

【HarmonyOS应用开发】Web组件的使用(十三)

文章末尾含&#xff1a;Web组件抽奖案例&#xff08;ArkTS&#xff09;-示例源码下载 Web组件的使用 一、概述 相信大家都遇到过这样的场景&#xff0c;有时候我们点击应用的页面&#xff0c;会跳转到一个类似浏览器加载的页面&#xff0c;加载完成后&#xff0c;才显示这个页…

数据结构 归并排序详解

1.基本思想 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。 将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff0c;即先使每个子序列有序…

05:容器镜像技术揭秘|发布容器服务器|私有镜像仓库

容器镜像技术揭秘&#xff5c;发布容器服务器&#xff5c;私有镜像仓库 创建镜像使用commit方法创建自定义镜像。Dockerfile打包镜像创建apache服务镜像制作 php 镜像 微服务架构创建nginx镜像 发布服务通过映射端口发布服务容器共享卷 docker私有仓库 创建镜像 使用commit方法…

elk之基本crud

写在前面 本文看下工作中用的最多的CRUD。让我们一起来做一个帅帅的CRUD BOY吧&#xff01;&#xff01;&#xff01; 1&#xff1a;基本操作 Create 格式1(指定ID)&#xff1a;PUT 索引名称/_create/文档ID {文档json} 格式2&#xff08;不指定ID&#xff09;:POST 索引名称…

【Algorithms 4】算法(第4版)学习笔记 03 - 1.3 背包、队列和栈

文章目录 前言参考目录学习笔记0&#xff1a;预热1&#xff1a;栈1.1&#xff1a;栈的链表实现1.1.1 代码实现1.2&#xff1a;栈的数组实现1.2.1&#xff1a;定容栈1.2.2&#xff1a;可调整大小数组1.2.3&#xff1a;代码实现1.3&#xff1a;链表与数组的取舍2&#xff1a;队列…

《吐血整理》高级系列教程-吃透Fiddler抓包教程(37)-掌握Fiddler中Fiddler Script用法,你会有多牛逼-下篇

1.简介 Fiddler是一款强大的HTTP抓包工具&#xff0c;它能记录所有客户端和服务器的http和https请求&#xff0c;允许你监视&#xff0c;设置断点&#xff0c;甚至修改输入输出数据. 使用Fiddler无论对开发还是测试来说&#xff0c;都有很大的帮助。Fiddler提供的功能基本上能满…

【LVGL环境搭建】

LVGL环境搭建 win模拟器环境搭建一.二.三.四.五. Ubuntu模拟器环境搭建一. 前置准备二. 下载LVGL Source code&#xff1a;三. 安装sdl2&#xff1a;四. 开启VScode执行五. 安装扩展套件六. 按F5执行七. 执行结果 win模拟器环境搭建 一. 二. 三. 四. 五. Ubuntu模拟器环境…

基于muduo网络库开发服务器程序和CMake构建项目 笔记

跟着施磊老师做C项目&#xff0c;施磊老师_腾讯课堂 (qq.com) 一、基于muduo网络库开发服务器程序 组合TcpServer对象创建EventLoop事件循环对象的指针明确TcpServer构造函数需要什么参数,输出ChatServer的构造函数在当前服务器类的构造函数当中,注册处理连接的回调函数和处理…

介绍msvcp140.dll丢失的解决方法的关键方法,关于msvcp140.dll文件

在使用电脑过程中&#xff0c;我们有时会遇到一些错误提示&#xff0c;比如msvcp140.dll丢失的问题。而这个问题的解决方法对于用户来说就显得尤为重要。本文旨在为大家简要介绍解决msvcp140.dll丢失问题的关键方法。 一.msvcp140.dll丢失的解决方法的详细步骤教程 重新安装相…

Leetcode 热门百题斩(第二天)

介绍 针对leetcode的热门一百题&#xff0c;解决大多数实习生面试的基本算法题。通过我自己的思路和多种方法&#xff0c;供大家参考。 1.两数之和&#xff08;题号&#xff1a;1) 方法一 最先想到的就是两个for去遍历匹配。 class Solution {public int[] twoSum(int[]…

C语言标准库所有字符串操作库函数汇总

以下是C语言标准库中字符串操作相关的API列表&#xff0c;这些函数通常在 <string.h> 头文件中定义&#xff1a; 1. strlen - 计算字符串长度&#xff0c;不包括结尾的空字符\0&#xff1a; size_t strlen(const char *str); 2. strcpy - 复制字符串&#xff1a; c…

Spring Bean 生命周期常见错误

虽然说 Spring 容器上手简单&#xff0c;可以仅仅通过学习一些有限的注解&#xff0c;即可达到快速使用的目的。但在工程实践中&#xff0c;我们依然会从中发现一些常见的错误。尤其当你对 Spring 的生命周期还没有深入了解时&#xff0c;类初始化及销毁过程中潜在的约定就不会…

HAL库配置片内FLASH读写

一、FLASH简介 不同型号的 STM32F40xx/41xx&#xff0c;其 FLASH 容量也有所不同&#xff0c;最小的只有 128K 字节&#xff0c;最大 的则达到了 1024K 字节。我们的探索者开发板选择的是 STM32F407ZGT6 的 FLASH 容量为 1024K 字节。 主存储器&#xff0c;存放代码和数据常数&…

PHP的线程安全与非线程安全模式选哪个

曾经初学PHP的时候也很困惑对线程安全与非线程安全模式这块环境的选择&#xff0c;也未能理解其中意。近来无意中看到一个教程对线程安全&#xff08;饿汉式&#xff09;&#xff0c;非线程安全&#xff08;懒汉式&#xff09;的描述&#xff0c;虽然觉得现在已经能够很明了透彻…

openlayers地图自定义图标打点(二)

1.效果 2.代码 结构,地图初始化同上篇 <template><div class"container">//地图结构<div id"map"></div></div> </template><script> //引入依赖项 import { Map, View } from ol import TileLayer from ol/l…

模板讲解之进阶

在之前的C入门的博客中我们就学习到了模板初阶&#xff0c;今天我们来学习模板的进阶&#xff0c;以便于更好地将模板运用到代码中 非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的…

关于v8垃圾回收机制联想到的知识点

对于值类型b来说&#xff0c;就直接释放了其占用的内存&#xff0c;对于引用类型obj来说&#xff0c;销毁的只是变量obj对堆内存地址 1001 的引用&#xff0c;obj的值 { c: 3 } 依然存在于堆内存中。那么堆内存中的变量如何进行回收呢&#xff1f; V8的垃圾回收策略主要是基于…

怎么录制屏幕视频?让你的视频脱颖而出

随着科技的飞速发展&#xff0c;录制屏幕视频已经成为人们日常学习和工作中不可或缺的技能。无论是制作教程、分享游戏高光时刻&#xff0c;还是保存线上会议的内容&#xff0c;屏幕录制都可以帮助我们更好地传达信息。可是怎么录制屏幕视频呢&#xff1f;本文将介绍两种录制屏…

手把手教你如何将项目发布到Maven中央仓库(附步骤及常见问题解决方法)

手把手教你如何将项目发布到Maven中央仓库(附步骤及常见问题解决方法) 业余时间写了个轻量级的权限控制框架 light-security &#xff0c;并发布到了 Maven 中央仓库。发布时的操作步骤还挺多&#xff0c;我这个记性是记不住的&#xff0c;所以记录一下&#xff0c;便于以后查…