Flink 实时数仓(四)【DWD 层搭建(二)流量域事实表】
前言
昨天刚搬到新校区,新校区小的可怜,好在之后出去实习交通可以方便点;待在学院太受限了,早点离开!
今天开始完成 DWD 层剩余的需求,上一节我们把日志数据根据不同类型分流写入到了不同的主题;
1、流量域独立访客事务事实表
独立访客指的其实就是我们 web 端日志分析指标中常说的 UV,上一节我们已经把页面日志写入到 dwd_page_traffic_log 主题当中了,所以这里我们直接对这个主题进行消费处理;
1.1、实现思路
既然是独立访客,就必须对日志中的数据做去重(独立访客数一般用来做日活指标,因为我们的机器一般都是 24 小时全年无休的,所以我们实时数仓也可以做这种日级别的指标需求,通过状态来存储历史就可以实现),而怎么判断访客是否重复?这就又用到了 Flink 中的状态编程(状态就是历史);和上一节我们判断新老访客一样,我们这里也可以给每个 mid 维护一个名为 lastVisitDate 的 ValueState(对 mid 进行 keyby),存储上一次访问的日期(注意是日期,只精确到天),每来一条数据就判断它的 lastVisitDate:
- 如果 lastVisitDate 为 null 或者 不是今天,则保留数据,否则丢弃
一旦进入第二天,lastVisitDate 状态就应该被清空(设置状态 TTL 为 1 天)
此外,对于 0 点的数据我们这里需要明确统计规则:
- 独立访客数据对应的页面必然是会话起始页面,last_page_id 必为 null;所以对于跨天的访问不能计算在内(昨天到今天访问了多个页面,而今天页面的 last_page_id 必然不为 null),我们需要在消费数据后的第一步就需要进行过滤;
1.2、代码实现
public class DwdTrafficUniqueVisitorDetail {public static void main(String[] args) throws Exception {// TODO 1. 获取执行环境StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1); // 生产环境中设置为kafka主题的分区数// 1.1 开启checkpointenv.enableCheckpointing(5 * 60000L, CheckpointingMode.EXACTLY_ONCE);env.getCheckpointConfig().setCheckpointStorage("hdfs://hadoop102:8020/s/ck");env.getCheckpointConfig().setCheckpointTimeout(10 * 60000L);env.getCheckpointConfig().setMaxConcurrentCheckpoints(2); // 设置最大共存的checkpoint数量env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,5000L)); // 固定频率重启: 尝试3次重启,每5s重启一次// 1.2 设置状态后端env.setStateBackend(new HashMapStateBackend());// TODO 2. 消费 kafka dwd_traffic_page_log 主题String topic = "dwd_traffic_page_log";String groupId = "uvDetail";DataStreamSource<String> pageDS = env.addSource(MyKafkaUtil.getFlinkKafkaConsumer(topic, groupId));// TODO 3. 过滤 last_page_id != Null 的数据// 使用 flatMap 而没用 filter,因为 flatMap 可以把过滤和转json 两步都一起完成SingleOutputStreamOperator<JSONObject> jsonDS = pageDS.flatMap(new FlatMapFunction<String, JSONObject>() {@Overridepublic void flatMap(String value, Collector<JSONObject> out) throws Exception {try {JSONObject jsonObject = JSONObject.parseObject(value);// 获取 last_page_idString last_page_id = jsonObject.getJSONObject("page").getString("last_page_id");if (last_page_id == null) {out.collect(jsonObject);}} catch (Exception e) {e.printStackTrace();}}});// TODO 4. 按照 mid 分组KeyedStream<JSONObject, String> keyedStream = jsonDS.keyBy(json -> json.getJSONObject("common").getString("mid"));// TODO 5. 使用状态编程实现按照 mid 的日期进行去重// 使用富函数,因为富函数提供更多的信息如上下文等SingleOutputStreamOperator<JSONObject> uvDS = keyedStream.filter(new RichFilterFunction<JSONObject>() {private ValueState<String> lastVisitDate = null;@Overridepublic void open(Configuration parameters) throws Exception {ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("lastVisit", String.class);lastVisitDate = getRuntimeContext().getState(stateDescriptor);}@Overridepublic boolean filter(JSONObject value) throws Exception {// 获取状态数据 & 当前数据中的时间并转为日期String lastDate = lastVisitDate.value();Long ts = value.getLong("ts");String curDate = DateFormatUtil.toDate(ts);if (lastDate == null || !lastDate.equals(curDate)) {// 更新状态lastVisitDate.update(curDate);return true;}return false;}});// TODO 6. 数据写入 kafkaString targetTopic = "dwd_traffic_unique_visitor_detail";uvDS.map(data -> data.toJSONString()).addSink(MyKafkaUtil.getFlinkKafkaProducer(targetTopic));// TODO 7. 执行任务env.execute("DwdTrafficUniqueVisitorDetail");}}
1.3、TTL 优化
上面我们的代码逻辑看起来已经没什么问题了,但是我们可以设想:假设一个用户,2024-01-01 首次登录之后,它的 lastVisitDate 状态会一直存储 2024-01-01,如果他下一次登录是在 2024-12-31,那么期间的 364 天我们依然要一直存储它的状态;而我们判断用户是否已经登录的逻辑是:lastVisitDate 是否为null 或者 lastVisitDate<今天,所以我们完全可以在一天之后把该用户的 lastVisitDate 状态清空,来减少状态的保存开销!
TTL 是给状态描述器设置的,而状态描述器是构造状态对象的必须参数!
TTL 是状态的一个属性,当我们修改状态值的时候,TTL 本身并不会更新!这里,我们需要在状态描述器中设置 TTL 的更新策略为创建或更新状态值的时候就更新 TTL ,重新开始过期倒计时;
我们只需要修改上面第 5 步,在初始化状态时,在状态描述器中给状态添加 TTL 属性:
@Overridepublic void open(Configuration parameters) throws Exception {ValueStateDescriptor<String> stateDescriptor = new ValueStateDescriptor<>("lastVisit", String.class);// 给状态添加 TTLstateDescriptor.enableTimeToLive(new StateTtlConfig.Builder(Time.days(1))// 设置 TTL 可更新,并且在创建或更新状态的时候更新.setUpdateType(StateTtlConfig.UpdateType.OnCreateAndWrite).build());lastVisitDate = getRuntimeContext().getState(stateDescriptor);}
2、流量域用户跳出事务事实表
跳出的概念:跳出指的是用户在一次会话中只访问了一个页面的情况(注意:粒度是会话),我们在之前做离线数仓的时候做过跳出率的指标,对于离线数仓,我们可以在 DWS 层构建一张流量域近1日会话粒度页面浏览表(dws_traffic_session_page_view_1d),通过下面的 SQL 就可以统计出该指标:
SELECT
CAST(SUM(IF(page_count=1,1,0))/COUNT(*)) AS DECIMAL(16,2) AS bounce_rate
FROM dws_traffic_session_page_view_1d
在这里的实时数仓中,我们不可能等到一天结束最后才去计算跳出率;但是我们这里又没有 session_id,所以我们只能换一种思路:
思路1(会话窗口):
- 使用会话窗口,为每个 mid 开启一个会话窗口并指定间隔为 10 s;一旦到了 10s 触发窗口关闭,计算窗口内的数据条数,> 1 条则说明这次会话没有发生跳出;
这种思路的问题很明显:① 如果我短时间(10s内)发生多个跳出,但是正好这些跳出都在一个会话,这会导致窗口结束时误以为这不是跳出,毕竟窗口内有多条数据;② 可能我的一次正常的会话,被会话窗口切分到两个不同的会话窗口,结果把一个非跳出访问计算为 2 个跳出访问;
思路2(状态编程):
在离线数仓中,当我们没有 session_id 时,我们可以一天的数据按照 mid 进行分组,然后根据时间戳字段进行排序,这样来计算一个 session;但是这里是实时数仓,我们不知道什么时候一个 session 会结束,所以我们可以设置一个定时器,定时器时间范围内的数据如果没数据来就视作一个会话结束,触发计算;并结合状态编程,把新会话的首页存入状态
- 遇到 last_page 为 null 的数据就试着取出状态
- 如果状态为 null,则该页面是新的会话起始页,开启定时器并将数据自身写入状态
- 如果状态不为 null,说明刚跳出一次,并且在定时器时间范围内又进来一次;这种情况需要将第一条数据(跳出的数据,也就是写入状态中的数据)输出,然后将自身写入状态,定时器依然存在,等时间到了触发计算
- 如果 last_page 不为 null,则状态中的数据和该条数据都丢弃
这种思路同样存在问题,当数据是乱序的时候一切都就乱套了;
思路3(Flink CEP):
Flink CEP 其实就是使用 状态编程 + within 开窗 来处理这种复杂事件
Flink CEP 定义的规则之间的连续策略
- 严格连续: 期望所有匹配的事件严格的一个接一个出现,中间没有任何不匹配的事件。对应方法为 next();
- 松散连续: 忽略匹配的事件之间的不匹配的事件。对应方法为followedBy();
- 不确定的松散连续: 更进一步的松散连续,允许忽略掉一些匹配事件的附加匹配。对应方法为followedByAny()。
定义模式之前的代码
这里需要注意:因为我们后面要保证数据有序,所以我们最好指定事件时间的提取字段,并添加水位线设置合理的超时时间(理论上可以保证数据绝对有序):
public class DwdTrafficUserJumpDetail {public static void main(String[] args) {// TODO 1. 获取执行环境StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1); // 生产环境中设置为kafka主题的分区数// 1.1 开启checkpointenv.enableCheckpointing(5 * 60000L, CheckpointingMode.EXACTLY_ONCE);env.getCheckpointConfig().setCheckpointStorage("hdfs://hadoop102:8020/s/ck");env.getCheckpointConfig().setCheckpointTimeout(10 * 60000L);env.getCheckpointConfig().setMaxConcurrentCheckpoints(2); // 设置最大共存的checkpoint数量env.setRestartStrategy(RestartStrategies.fixedDelayRestart(3,5000L)); // 固定频率重启: 尝试3次重启,每5s重启一次// 1.2 设置状态后端env.setStateBackend(new HashMapStateBackend());// TODO 2. 消费 kafka dwd_traffic_page_log 主题String topic = "dwd_traffic_page_log";String groupId = "user_jump_detail";DataStreamSource<String> pageDS = env.addSource(MyKafkaUtil.getFlinkKafkaConsumer(topic, groupId));// TODO 3. 将数据转为 JSONSingleOutputStreamOperator<JSONObject> jsonDS = pageDS.map(JSON::parseObject);// TODO 4. 提取事件时间 & 按照 mid 分组KeyedStream<JSONObject, String> keyedStream = jsonDS.assignTimestampsAndWatermarks(WatermarkStrategy.<JSONObject>forBoundedOutOfOrderness(Duration.ofSeconds(2)).withTimestampAssigner(new SerializableTimestampAssigner<JSONObject>() {@Overridepublic long extractTimestamp(JSONObject element, long recordTimestamp) {return element.getLong("ts");}})).keyBy(json -> json.getJSONObject("common").getString("mid"));
接下来是核心的定义 CEP 模式的代码:
// TODO 5. 定义 CEP 模式序列// 泛型方法类型指的是流的类型(下面的 start 和 next 作为提取事件的 key)Pattern<JSONObject, JSONObject> pattern = Pattern.<JSONObject>begin("start").where(new SimpleCondition<JSONObject>() {@Overridepublic boolean filter(JSONObject value) throws Exception {return value.getJSONObject("page").getString("last_page_id") == null;}}).next("next").where(new SimpleCondition<JSONObject>() {@Overridepublic boolean filter(JSONObject value) throws Exception {return value.getJSONObject("page").getString("last_page_id") == null;}}).within(Time.seconds(10L));// 等价于 循环模式 共用一个 key: start Pattern.<JSONObject>begin("start").where(new SimpleCondition<JSONObject>() {@Overridepublic boolean filter(JSONObject value) throws Exception {return value.getJSONObject("page").getString("last_page_id") == null;}}).times(2) // 默认是宽松近邻 followedBy.consecutive() // 严格近邻 next.within(Time.seconds(10L));// TODO 6. 建模式序列作用到流上PatternStream<JSONObject> patternStream = CEP.pattern(keyedStream, pattern);// TODO 7. 提取事件(匹配上的时间 和 超时时间)OutputTag<String> timeoutTag = new OutputTag<>("timeout");SingleOutputStreamOperator<String> selectDS = patternStream.select(timeoutTag,// 超时数据new PatternTimeoutFunction<JSONObject, String>() {// 对于超时数据来说,当前的数据第一个规则匹配上了,第二个没有匹配上导致超时,那么我们要提取的就是当前数据(第一个数据,第二个数据没来)// 这里的 Map 的 v 是 List 数据类型,因为考虑到我们可能使用的是循环模式(只有一个key)@Overridepublic String timeout(Map<String, List<JSONObject>> map, long l) throws Exception {return map.get("start").get(0).toJSONString();}}, // 匹配上的数据new PatternSelectFunction<JSONObject, String>() {// 匹配上的数据,我们只要第一个数据,因为只能证明第一个数据是跳出数据@Overridepublic String select(Map<String, List<JSONObject>> map) throws Exception {return map.get("start").get(0).toJSONString();}});DataStream<String> timeoutDS = selectDS.getSideOutput(timeoutTag);// TODO 8. 合并两种事件DataStream<String> unionDS = selectDS.union(timeoutDS);// TODO 9. 合并后的数据写入 kafkaString targetTopic = "dwd_traffic_user_jump_detail";unionDS.addSink(MyKafkaUtil.getFlinkKafkaProducer(targetTopic));// TODO 10. 启动任务env.execute("DwdTrafficUserJumpDetail");}
}
上面我们定义了两种匹配规则:
- 第一条数据的 last_page_id 为 null ,且超时没有收到第二条数据,认定该条数据为跳出数据
- 第二条数据的 last_page_id 为 null ,则认定第一条数据是跳出数据
超时时间内规则一被满足,未等到第二条数据则会被判定为超时数据。所以我们只要把超时数据和 满足连续两条数据的 last_page_id 均为 null 中的第一条数据 union 起来,得到的即为答案所需数据;
总结
至此,流量域的三个需求都已经完成;
相关文章:

Flink 实时数仓(四)【DWD 层搭建(二)流量域事实表】
前言 昨天刚搬到新校区,新校区小的可怜,好在之后出去实习交通可以方便点;待在学院太受限了,早点离开! 今天开始完成 DWD 层剩余的需求,上一节我们把日志数据根据不同类型分流写入到了不同的主题;…...
Go 1.19.4 接口-Day 10
1. 接口 1.1 基本介绍 接口中到底应该定义些什么? 在Go语言中,接口是声明函数的集合,但只有函数签名,没有具体的功能。 属于是面向对象中,行为的约束,面向对象中的类有自己的属性(可以当成数据…...

智能编程新纪元:腾讯AI代码助手的高效编程体验
智能编程新纪元:腾讯AI代码助手的高效编程体验 智能编程新纪元:腾讯AI代码助手的高效编程体验引言一、配置开发环境二、AI助手实现高效编程2.1 AI助手自动补全2.2 AI助手实现编程思维2.3 AI助手高效注解2.4 AI助手打破语言壁垒 三、帮助和提升四、优化和…...

使用snap安装docker配置阿里云镜像加速
使用snap安装docker非常的简单,一条命令即可 snap install docker 但是通过这个命令安装的docker, 配置阿里云镜像跟常规安装的配置起来不太一样, 下面讲一下配置流程 修改docker配置文件/var/snap/docker/current/config/daemon.json 这个文件应该是已经创建好…...
安全编程的代码示例
一、python代码示例 一个安全编程的示例是对输入进行严格的验证和过滤。比如,当用户在网页上输入用户名和密码时,应该对这些输入进行验证,防止恶意用户输入恶意代码或进行 SQL 注入等攻击。下面是一个简单的示例代码: import…...

【实现100个unity特效之15】最简单的方法使用shader graphs实现2d非像素和像素树叶草的随风摇摆效果
文章目录 前言非像素树叶草飘动效果新建材质效果像素树叶草飘动效果参考完结 前言 本文只是实现一个简单版本的2d树叶草随风摇摆的效果,如果你想要实现更加复杂的效果,包括2d互动草,你可以参考我之前的文章: 【推荐100个unity插件…...

Vue3+TS+element plus实现一个简单列表页面
期望完成效果 1.创建一个api api内容: 根据接口: 修改 url 和 函数的参数 以及 params里的内容 import { request } from "/utils/service" /** 查 */ export function getDyLogDataApi(page: any, limit: any, campaign_id: any, adgroup_id…...

Go语言中gin+gorm开发前端端分离博客时遇到的问题,gorm执行查询时如何选中特定字段?
代码获取 本篇文章的代码放在了Github上,可以免费获取。 https://github.com/zhangdapeng520/zdpgo_gin_examples 概述 在查询用户信息的时候,由于密码这个字段比较敏感,需要进行处理,不要返回给前端。 我一开始的解决方案是直…...
计算机网络11——数据库语法2
1、变量 (1)局部变量 函数里面定义的,变量名 类型 (2)会话变量 本次连接会话有效,不需要定义声明,直接使用,@变量名 类型 set @x=10; select @x;(3)系统变量 又叫全局变量,只有root变量才能使用,一直有效。因为全局变量影响服务器运行,所以Mysql不允许自定…...

华为USG6000E-S12防火墙Key exchange failed.无法SSH解决方案
由于目前防火墙算法太新,导致crt和xshell的版本无法登陆,按以下方法解决 一、下载华为本地加载除弱安全算法组件包之外的组件包 二、先改后缀名为.cfg,上传文件到防火墙 三、在用户视图下改后缀名为.mod 四、move 文件到$_install_mod文件夹 五、执行…...
matlab基础操作(五)
31.数组维数的减小 >> amagic(4),a(:,2)[] >> a(1,2)[] 带有下标的赋值维度不匹配。 >> a(2:4)[]%数组a将变为向量 32.元胞数组的创建 Cell indexing方式创建元胞数组 >> c(1,1){[1 4 3;0 5 8;7 2 9]} >> c(1,2){Anne Smith} >> c(2,1){…...

力扣 两数之和
致每一个初学算法的你。 题目 时间复杂度:O(N^2), 空间复杂度:O(1) 。 class Solution {public int[] twoSum(int[] nums, int target) {int n nums.length;for (int i 0; i < n; i) {for (int j i 1; j < n; j) {if (nums[i] …...
Django 实现连续请求
创作灵感:工作中,前端因为某些原因(极其特殊)无法发送两个请求,需要后端实现 言归正传: 背景:使用djangoapscheduler实现定时任务,现在创建任务以及启动任务为两个接口,…...

前端(react)框架nextjs
文章目录 一、什么是next.js1. 路由2. 打包 next build3. 部署 二、 next.js 和react区别三、webstorm使用nextjs四、开发常用总结如何修nextjs 启动监听的端口号?NGINX 反向代理 Next.js 项目配置 参考 一、什么是next.js 官网: https://www.nextjs.cn…...

深耕编程语言18年,对话 Rust、TypeScript、Nushell 核心贡献者 Sophia Turner | Open AGI Forum
作者 | Annie Xu 采访 | 卢威 责编 | Echo Tang 出品丨GOSIM 开源创新汇 编程语言的种类令人眼花缭乱,但成功的、常用的编程语言却是凤毛麟角。在深耕编程语言研发 18 年的 Sophia June Turner 看来,编程语言成功的关键在于其研发团队的透明度和机制建…...

深度学习--图像分割UNet介绍及代码分析
UNet介绍 参考UNet网络介绍整体架构UNet过程输入编码器(下采样)中间特征表示解码器(上采样)输出 代码详解unetUP和Unet关系上采样模块——unetUp用于图像分割的卷积神经网络(CNN)架构模块——Unet类的定义初…...

接了一个2000块的小活,大家进来看看值不值,附源码
如题,上周的一天,朋友圈的一个旧友找到了我,说让我帮他开发一个小工具,虽然活不大,但没个几年的全栈经验还不一定能接下来,因为麻雀虽小,涉及的内容可不少: 需求分析 原型设计 详细…...

基于MindFormers实现GPT2模型的推理
前言 针对MindFormers的安装,可参考本专栏里的另一篇博客 安装MindFormers(昇腾910)-CSDN博客 pipeline方式 from mindformers import pipeline from mindformers import GPT2LMHeadModel, GPT2Config, GPT2Tokenizer tok GPT2Tokenizer…...

探索腾讯云AI代码助手:智能编程的新时代
智能编程的新时代 前言开发环境介绍腾讯云 AI 代码助手使用实例生成文档解释代码生成测试修复代码人工智能技术对话 智能编程获得的帮助与提升对腾讯云AI代码助手的建议结语 前言 hello,大家好我是恒川,今天我来给大家安利一款非常好用的AI 代码助手&…...
MySQL 之 MHA 高可用架构详解
这个是在内部分享做的一个 Keynote 动画,用来演示 MHA 高可用架构及发生故障时的 Failover。动画如下: 数据库相关分享之 MySQL 的 MHA 架构详解 引言 MySQL 数据库在企业和应用中扮演着举足轻重的角色,其稳定性和可靠性对于业务的连续运行至…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...

国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

3-11单元格区域边界定位(End属性)学习笔记
返回一个Range 对象,只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意:它移动的位置必须是相连的有内容的单元格…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

人工智能(大型语言模型 LLMs)对不同学科的影响以及由此产生的新学习方式
今天是关于AI如何在教学中增强学生的学习体验,我把重要信息标红了。人文学科的价值被低估了 ⬇️ 转型与必要性 人工智能正在深刻地改变教育,这并非炒作,而是已经发生的巨大变革。教育机构和教育者不能忽视它,试图简单地禁止学生使…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...

【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
如何通过git命令查看项目连接的仓库地址?
要通过 Git 命令查看项目连接的仓库地址,您可以使用以下几种方法: 1. 查看所有远程仓库地址 使用 git remote -v 命令,它会显示项目中配置的所有远程仓库及其对应的 URL: git remote -v输出示例: origin https://…...