深度解析RocketMq源码-高可用存储组件(四)Dledger框架日志同步流程
1.绪论
在深度解析RocketMq源码-高可用存储组件(一) raft协议详解-CSDN博客 中讲过,raft协议中,日志同步主要有两个地方,一个是leader会跟follower同步数据,另一个是在新leader诞生的时候,会与以前的follower进行日志匹配,作一致性校验。而在Dledger的进行日志校验的组件就是DLedgerEntryPusher,接下来我们将探索该组件的源码。
2.DLedgerEntryPusher的组成
数据同步组件,主要是leader写入数据后,将消息同步给follower & 一致性检查过后,leader与消息保持索引一致
//数据同步组件,主要是leader写入数据后,将消息同步给follower & 一致性检查过后,leader与消息保持索引一致
public class DLedgerEntryPusher {private static Logger logger = LoggerFactory.getLogger(DLedgerEntryPusher.class);//Dledger的配置private DLedgerConfig dLedgerConfig;//所属的存储组件private DLedgerStore dLedgerStore;//当前节点的元信息private final MemberState memberState;//负责网络请求的组件private DLedgerRpcService dLedgerRpcService;//key:term <key:peerId value:每个阶段最后的一条日志Index>private Map<Long, ConcurrentMap<String, Long>> peerWaterMarksByTerm = new ConcurrentHashMap<>();//key:term <key:日志的index id value:写入结果,是一个future>private Map<Long, ConcurrentMap<Long, TimeoutFuture<AppendEntryResponse>>> pendingAppendResponsesByTerm = new ConcurrentHashMap<>();//follower使用 接收leader的日志同步,然后按照顺序写入到日志文件中private EntryHandler entryHandler;//raft的日志写入是通过二阶段提交实现,如果第一阶段,超过半数follower写入成功,就认为数据写入成功,该组件就是检查是否follower是否写入成功的private QuorumAckChecker quorumAckChecker;//leader使用 将不同的命令同步给followerprivate Map<String, EntryDispatcher> dispatcherMap = new HashMap<>();//将命令应用到状态机的组件private Optional<StateMachineCaller> fsmCaller;
}
3.DLedger每条消息的组成结构-DLedgerEntry
可以看出每条消息都包含term和index这两个关键信息
public class DLedgerEntry {public final static int POS_OFFSET = 4 + 4 + 8 + 8;public final static int HEADER_SIZE = POS_OFFSET + 8 + 4 + 4 + 4;public final static int BODY_OFFSET = HEADER_SIZE + 4;//魔数private int magic;//消息大小private int size;//日志索引private long index;//termprivate long term;//物理位置private long pos; //used to validate dataprivate int channel; //reservedprivate int chainCrc; //like the block chain, this crc indicates any modification before this entry.private int bodyCrc; //the crc of the body//真正的消息体private byte[] body;
}
4.同步消息的步骤
4.1 leader发送同步请求-EntryDispatcher
leader节点会给每个follower构建一个EntryDispatcher,专门用来进行数据同步。
4.1.1 组件组成
该组件是follower同步leader数据的组件,有4种可能:
1.APPEND: 将消息同步到follower的commitLog中;
2.COMPARE: 当leader发生变更,leader会与所有的follower进行一致性检查;
3.TRUNCATE: 如果leader检查完成过后,发现follower的最后一条索引更大,需要将他删除掉;
4.COMMIT: 当leader超过一半的节点都append日志成功过后,leader会进行commit,这个时候会发送
//该组件是follower同步leader数据的组件,有4种可能
//1.APPEND:将消息同步到follower的commitLog中//2.COMPARE: 当leader发生变更,leader会与所有的follower进行一致性检查//3.TRUNCATE:如果leader检查完成过后,发现follower的最后一条索引更大,需要将他删除掉//4.COMMIT:当leader超过一半的节点都append日志成功过后,leader会进行commit,这个时候会发送一个请求通知所有的follower进行commit
private class EntryDispatcher extends ShutdownAbleThread {//需要对日志进行处理的类型private AtomicReference<PushEntryRequest.Type> type = new AtomicReference<>(PushEntryRequest.Type.COMPARE);//上一次同步日志的时间private long lastPushCommitTimeMs = -1;//节点id 给哪个节点使用的private String peerId;//已经发送过比较请求的索引private long compareIndex = -1;//已经发送过同步请求的索引private long writeIndex = -1;//最大有多少条日志没有同步private int maxPendingSize = 1000;//周期private long term = -1;//leader的idprivate String leaderId = null;//上一次check的时间private long lastCheckLeakTimeMs = System.currentTimeMillis();//同步索引过后,会在次等待结果
}
4.1.2 主要功能
其实是一个死循环,一直调用doWork()方法
public void doWork() {try {//判断当前节点是否是leader节点if (!checkAndFreshState()) {waitForRunning(1);return;}if (type.get() == PushEntryRequest.Type.APPEND) {if (dLedgerConfig.isEnableBatchPush()) {doBatchAppend();} else {//如果是append进行append超过doAppend();}} else {//如果是一致性检测进行doComparedoCompare();}waitForRunning(1);} catch (Throwable t) {DLedgerEntryPusher.logger.error("[Push-{}]Error in {} writeIndex={} compareIndex={}", peerId, getName(), writeIndex, compareIndex, t);changeState(-1, PushEntryRequest.Type.COMPARE);DLedgerUtils.sleep(500);}}}
4.1.3 同步日志步骤
1.发送日志预提交请求
//同步那条索引的数据
private void doAppendInner(long index) throws Exception {//根据index获取到commitLog中的数据,其实首先从indexfile中获取到日志的具体的物理偏移量,再根据物理偏移量获取到对应的数据DLedgerEntry entry = getDLedgerEntryForAppend(index);if (null == entry) {return;}//流量控制checkQuotaAndWait(entry);//构建append请求PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.APPEND);//通过网络请求,发送请求CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request);//加入peddingMap中,便是第index条索引正在同步pendingMap.put(index, System.currentTimeMillis());responseFuture.whenComplete((x, ex) -> {try {PreConditions.check(ex == null, DLedgerResponseCode.UNKNOWN);DLedgerResponseCode responseCode = DLedgerResponseCode.valueOf(x.getCode());switch (responseCode) {case SUCCESS:pendingMap.remove(x.getIndex());//如果发送请求成功 便更新当前节点已经发送请求的最新的一条索引updatePeerWaterMark(x.getTerm(), peerId, x.getIndex());quorumAckChecker.wakeup();break;case INCONSISTENT_STATE:logger.info("[Push-{}]Get INCONSISTENT_STATE when push index={} term={}", peerId, x.getIndex(), x.getTerm());changeState(-1, PushEntryRequest.Type.COMPARE);break;default:logger.warn("[Push-{}]Get error response code {} {}", peerId, responseCode, x.baseInfo());break;}} catch (Throwable t) {logger.error("", t);}});//更新最后的同步时间lastPushCommitTimeMs = System.currentTimeMillis();
}
//构建日志的请求
private PushEntryRequest buildPushRequest(DLedgerEntry entry, PushEntryRequest.Type target) {PushEntryRequest request = new PushEntryRequest();//所属组request.setGroup(memberState.getGroup());//需要发送的节点idrequest.setRemoteId(peerId);//当前leaderIdrequest.setLeaderId(leaderId);//自己的idrequest.setLocalId(memberState.getSelfId());//当前周期request.setTerm(term);//日志内容request.setEntry(entry);//消息类型request.setType(target);//主节点中最新一条commit过的日志索引 通过它可以将已经提交过的索引带给follower,follower收到过后可以将该索引之间的日志进行提交request.setCommitIndex(dLedgerStore.getCommittedIndex());return request;
}
2.进行commit操作
private void doCommit() throws Exception {//如果超过上次1000ms,才提交commit请求 实现批量commit的功能if (DLedgerUtils.elapsed(lastPushCommitTimeMs) > 1000) {//发送commit请求PushEntryRequest request = buildPushRequest(null, PushEntryRequest.Type.COMMIT);//Ignore the resultsdLedgerRpcService.push(request);lastPushCommitTimeMs = System.currentTimeMillis();}
}
4.1.4 leader的一致性检测步骤
其实就是比较follower的最后一条索引的合法性,如果存在脏数据,便构建truncate请求删掉。如果比较失败,便将自己的最新的index减1发送到follower再次比对,直到成功。
private void doCompare() throws Exception {while (true) {if (!checkAndFreshState()) {break;}if (type.get() != PushEntryRequest.Type.COMPARE&& type.get() != PushEntryRequest.Type.TRUNCATE) {break;}if (compareIndex == -1 && dLedgerStore.getLedgerEndIndex() == -1) {break;}//revise the compareIndex//设置commpare的开始比较的索引为leader的写入额度最后一条索引if (compareIndex == -1) {compareIndex = dLedgerStore.getLedgerEndIndex();logger.info("[Push-{}][DoCompare] compareIndex=-1 means start to compare", peerId);} else if (compareIndex > dLedgerStore.getLedgerEndIndex() || compareIndex < dLedgerStore.getLedgerBeginIndex()) {logger.info("[Push-{}][DoCompare] compareIndex={} out of range {}-{}", peerId, compareIndex, dLedgerStore.getLedgerBeginIndex(), dLedgerStore.getLedgerEndIndex());compareIndex = dLedgerStore.getLedgerEndIndex();}//获取entryDLedgerEntry entry = dLedgerStore.get(compareIndex);PreConditions.check(entry != null, DLedgerResponseCode.INTERNAL_ERROR, "compareIndex=%d", compareIndex);//封装compare请求 & 发送请求PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.COMPARE);CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request);PushEntryResponse response = responseFuture.get(3, TimeUnit.SECONDS);PreConditions.check(response != null, DLedgerResponseCode.INTERNAL_ERROR, "compareIndex=%d", compareIndex);PreConditions.check(response.getCode() == DLedgerResponseCode.INCONSISTENT_STATE.getCode() || response.getCode() == DLedgerResponseCode.SUCCESS.getCode(), DLedgerResponseCode.valueOf(response.getCode()), "compareIndex=%d", compareIndex);long truncateIndex = -1;//如果比较成功,证明此时leader和follower的数据是一致的,但是follower的最后一条索引的位置大于leader的最后一条的位置的话,证明follower有脏数据,需要删除if (response.getCode() == DLedgerResponseCode.SUCCESS.getCode()) {/** The comparison is successful:* 1.Just change to append state, if the follower's end index is equal the compared index.* 2.Truncate the follower, if the follower has some dirty entries.*/if (compareIndex == response.getEndIndex()) {changeState(compareIndex, PushEntryRequest.Type.APPEND);break;} else {truncateIndex = compareIndex;}//如果follower的位置不在leader的索引位置之间,有脏数据,需要删除} else if (response.getEndIndex() < dLedgerStore.getLedgerBeginIndex()|| response.getBeginIndex() > dLedgerStore.getLedgerEndIndex()) {/*The follower's entries does not intersect with the leader.This usually happened when the follower has crashed for a long time while the leader has deleted the expired entries.Just truncate the follower.*/truncateIndex = dLedgerStore.getLedgerBeginIndex();} else if (compareIndex < response.getBeginIndex()) {/*The compared index is smaller than the follower's begin index.This happened rarely, usually means some disk damage.Just truncate the follower.*/truncateIndex = dLedgerStore.getLedgerBeginIndex();} else if (compareIndex > response.getEndIndex()) {/*The compared index is bigger than the follower's end index.This happened frequently. For the compared index is usually starting from the end index of the leader.*/compareIndex = response.getEndIndex();//如果比较失败,即follower的index在leader的index之间,但是小于leader的index,便leader的index-- 再进行比较} else {/*Compare failed and the compared index is in the range of follower's entries.*/compareIndex--;}/*The compared index is smaller than the leader's begin index, truncate the follower.*/if (compareIndex < dLedgerStore.getLedgerBeginIndex()) {truncateIndex = dLedgerStore.getLedgerBeginIndex();}/*If get value for truncateIndex, do it right now.*/if (truncateIndex != -1) {changeState(truncateIndex, PushEntryRequest.Type.TRUNCATE);doTruncate(truncateIndex);break;}}}
4.2 leader检测是否有半数以上follower写入成功-QuorumAckChecker
private class QuorumAckChecker extends ShutdownAbleThread {private long lastPrintWatermarkTimeMs = System.currentTimeMillis();private long lastCheckLeakTimeMs = System.currentTimeMillis();private long lastQuorumIndex = -1;public QuorumAckChecker(Logger logger) {super("QuorumAckChecker-" + memberState.getSelfId(), logger);}@Overridepublic void doWork() {try {if (DLedgerUtils.elapsed(lastPrintWatermarkTimeMs) > 3000) {if (DLedgerEntryPusher.this.fsmCaller.isPresent()) {//获取上次已经被应用到状态机的indexfinal long lastAppliedIndex = DLedgerEntryPusher.this.fsmCaller.get().getLastAppliedIndex();logger.info("[{}][{}] term={} ledgerBegin={} ledgerEnd={} committed={} watermarks={} appliedIndex={}",memberState.getSelfId(), memberState.getRole(), memberState.currTerm(), dLedgerStore.getLedgerBeginIndex(), dLedgerStore.getLedgerEndIndex(), dLedgerStore.getCommittedIndex(), JSON.toJSONString(peerWaterMarksByTerm), lastAppliedIndex);} else {logger.info("[{}][{}] term={} ledgerBegin={} ledgerEnd={} committed={} watermarks={}",memberState.getSelfId(), memberState.getRole(), memberState.currTerm(), dLedgerStore.getLedgerBeginIndex(), dLedgerStore.getLedgerEndIndex(), dLedgerStore.getCommittedIndex(), JSON.toJSONString(peerWaterMarksByTerm));}lastPrintWatermarkTimeMs = System.currentTimeMillis();}if (!memberState.isLeader()) {waitForRunning(1);return;}long currTerm = memberState.currTerm();checkTermForPendingMap(currTerm, "QuorumAckChecker");checkTermForWaterMark(currTerm, "QuorumAckChecker");if (pendingAppendResponsesByTerm.size() > 1) {//raft协议规定,每个leader只能commit自己term的请求,如果还存在其他周期待确认的日志,直接返回term已经改变,并且移除掉for (Long term : pendingAppendResponsesByTerm.keySet()) {if (term == currTerm) {continue;}for (Map.Entry<Long, TimeoutFuture<AppendEntryResponse>> futureEntry : pendingAppendResponsesByTerm.get(term).entrySet()) {AppendEntryResponse response = new AppendEntryResponse();response.setGroup(memberState.getGroup());response.setIndex(futureEntry.getKey());response.setCode(DLedgerResponseCode.TERM_CHANGED.getCode());response.setLeaderId(memberState.getLeaderId());logger.info("[TermChange] Will clear the pending response index={} for term changed from {} to {}", futureEntry.getKey(), term, currTerm);futureEntry.getValue().complete(response);}pendingAppendResponsesByTerm.remove(term);}}if (peerWaterMarksByTerm.size() > 1) {for (Long term : peerWaterMarksByTerm.keySet()) {if (term == currTerm) {continue;}logger.info("[TermChange] Will clear the watermarks for term changed from {} to {}", term, currTerm);peerWaterMarksByTerm.remove(term);}}//获取当前周期每个节点的最新一条发送成功的索引Map<String, Long> peerWaterMarks = peerWaterMarksByTerm.get(currTerm);//根据索引进行排序List<Long> sortedWaterMarks = peerWaterMarks.values().stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());//获取中间的数,比如现在有4个followerA B C D,分别已经发送成功的索引为[5 7 4 9],根据排序取中间数为5,一定是超过半数的节点发送成功的索引是大于5的,所以leader可以commitlong quorumIndex = sortedWaterMarks.get(sortedWaterMarks.size() / 2);final Optional<StateMachineCaller> fsmCaller = DLedgerEntryPusher.this.fsmCaller;if (fsmCaller.isPresent()) {// If there exist statemachine//如果存在状态机,便leader自己提交索引,并且将其应用到状态机DLedgerEntryPusher.this.dLedgerStore.updateCommittedIndex(currTerm, quorumIndex);final StateMachineCaller caller = fsmCaller.get();caller.onCommitted(quorumIndex);// Check elapsedif (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000) {updatePeerWaterMark(currTerm, memberState.getSelfId(), dLedgerStore.getLedgerEndIndex());checkResponseFuturesElapsed(caller.getLastAppliedIndex());lastCheckLeakTimeMs = System.currentTimeMillis();}if (quorumIndex == this.lastQuorumIndex) {waitForRunning(1);}} else {dLedgerStore.updateCommittedIndex(currTerm, quorumIndex);ConcurrentMap<Long, TimeoutFuture<AppendEntryResponse>> responses = pendingAppendResponsesByTerm.get(currTerm);boolean needCheck = false;int ackNum = 0;for (Long i = quorumIndex; i > lastQuorumIndex; i--) {try {CompletableFuture<AppendEntryResponse> future = responses.remove(i);if (future == null) {needCheck = true;break;} else if (!future.isDone()) {AppendEntryResponse response = new AppendEntryResponse();response.setGroup(memberState.getGroup());response.setTerm(currTerm);response.setIndex(i);response.setLeaderId(memberState.getSelfId());response.setPos(((AppendFuture) future).getPos());future.complete(response);}ackNum++;} catch (Throwable t) {logger.error("Error in ack to index={} term={}", i, currTerm, t);}}if (ackNum == 0) {checkResponseFuturesTimeout(quorumIndex + 1);waitForRunning(1);}if (DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000 || needCheck) {updatePeerWaterMark(currTerm, memberState.getSelfId(), dLedgerStore.getLedgerEndIndex());checkResponseFuturesElapsed(quorumIndex);lastCheckLeakTimeMs = System.currentTimeMillis();}}lastQuorumIndex = quorumIndex;} catch (Throwable t) {DLedgerEntryPusher.logger.error("Error in {}", getName(), t);DLedgerUtils.sleep(100);}}}
4.3 follower收到日志同步请求的处理
构建响应的步骤如下:
private PushEntryResponse buildResponse(PushEntryRequest request, int code) {//够级pushEntry的响应PushEntryResponse response = new PushEntryResponse();response.setGroup(request.getGroup());//设置响应码response.setCode(code);//当前周期response.setTerm(request.getTerm());if (request.getType() != PushEntryRequest.Type.COMMIT) {response.setIndex(request.getFirstEntryIndex());response.setCount(request.getCount());}//设置当前从节点的开始和结束索引response.setBeginIndex(dLedgerStore.getLedgerBeginIndex());response.setEndIndex(dLedgerStore.getLedgerEndIndex());return response;}
4.3.1 follower处理删除请求
private void doTruncate(long truncateIndex) throws Exception {PreConditions.check(type.get() == PushEntryRequest.Type.TRUNCATE, DLedgerResponseCode.UNKNOWN);//构建请求DLedgerEntry truncateEntry = dLedgerStore.get(truncateIndex);PreConditions.check(truncateEntry != null, DLedgerResponseCode.UNKNOWN);logger.info("[Push-{}]Will push data to truncate truncateIndex={} pos={}", peerId, truncateIndex, truncateEntry.getPos());//发送请求PushEntryRequest truncateRequest = buildPushRequest(truncateEntry, PushEntryRequest.Type.TRUNCATE);PushEntryResponse truncateResponse = dLedgerRpcService.push(truncateRequest).get(3, TimeUnit.SECONDS);PreConditions.check(truncateResponse != null, DLedgerResponseCode.UNKNOWN, "truncateIndex=%d", truncateIndex);PreConditions.check(truncateResponse.getCode() == DLedgerResponseCode.SUCCESS.getCode(), DLedgerResponseCode.valueOf(truncateResponse.getCode()), "truncateIndex=%d", truncateIndex);lastPushCommitTimeMs = System.currentTimeMillis();//删除完成过后,再把动作调整为appendchangeState(truncateIndex, PushEntryRequest.Type.APPEND);}
4.3.2 follower处理追加请求
//处理append
private void handleDoAppend(long writeIndex, PushEntryRequest request,CompletableFuture<PushEntryResponse> future) {try {PreConditions.check(writeIndex == request.getEntry().getIndex(), DLedgerResponseCode.INCONSISTENT_STATE);//调用appendAsFollower方法直接建数据写入到commitLog中DLedgerEntry entry = dLedgerStore.appendAsFollower(request.getEntry(), request.getTerm(), request.getLeaderId());PreConditions.check(entry.getIndex() == writeIndex, DLedgerResponseCode.INCONSISTENT_STATE);//够建立返回请求future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));//更新commitIndex,主节点会将自己在当前term的commitIndex发送过来updateCommittedIndex(request.getTerm(), request.getCommitIndex());} catch (Throwable t) {logger.error("[HandleDoAppend] writeIndex={}", writeIndex, t);future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));}}
4.3.3 follower处理一致性比较请求
private CompletableFuture<PushEntryResponse> handleDoCompare(long compareIndex, PushEntryRequest request,CompletableFuture<PushEntryResponse> future) {try {PreConditions.check(compareIndex == request.getEntry().getIndex(), DLedgerResponseCode.UNKNOWN);PreConditions.check(request.getType() == PushEntryRequest.Type.COMPARE, DLedgerResponseCode.UNKNOWN);//获取需要比较的索引数据,构建成响应返回给leaderDLedgerEntry local = dLedgerStore.get(compareIndex);PreConditions.check(request.getEntry().equals(local), DLedgerResponseCode.INCONSISTENT_STATE);future.complete(buildResponse(request, DLedgerResponseCode.SUCCESS.getCode()));} catch (Throwable t) {logger.error("[HandleDoCompare] compareIndex={}", compareIndex, t);future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));}return future;}
相关文章:
深度解析RocketMq源码-高可用存储组件(四)Dledger框架日志同步流程
1.绪论 在深度解析RocketMq源码-高可用存储组件(一) raft协议详解-CSDN博客 中讲过,raft协议中,日志同步主要有两个地方,一个是leader会跟follower同步数据,另一个是在新leader诞生的时候,会与…...

ONLYOFFICE 文档开发者版 8.1:API 更新
随着版本 8.1 新功能的发布,我们更新了编辑器、文档生成器和插件的 API,并添加了 Office API 板块。阅读下文了解详情。 ONLYOFFICE 文档是什么 ONLYOFFICE 文档是一个功能强大的文档编辑器,支持处理文本文档、电子表格、演示文稿、可填写…...
Activemq单节点在Windows下的配置部署
1.环境信息 服务器信息jdk版本activemq版本备注Windows Server 2008R2 Enterprisejdk-17_windows-x64_bin.exeapache-activemq-5.18.42.jdk配置 1.下载jdk 地址: Java Downloads | Oracle 中国 2.上传至Windows服务器,点击安装,在选择安装目录页面,选择合适的安装目录即…...

SpringBoot-注解@ImportResource引入自定义spring的配置xml文件和配置类
1、注解ImportResource 我们知道Spring的配置文件是可以有很多个的,我们在web.xml中如下配置就可以引入它们: SprongBoot默认已经给我们配置好了Spring,它的内部相当于已经有一个配置文件,那么我们想要添加新的配置文件怎么办&am…...

GitLab配置免密登录之后仍然需要Git登录的解决办法
GitLab配置免密登录之后仍然需要Git登录的解决办法 因为实习工作需要,要在本地拉取gitlab上的代码,设置了密钥之后连接的时候还需要登录的token,摸索之后有了下面的解决办法。 方法一: 根据报错的提示,去网站上设置个人…...

探索小众爱好:打造个人韧性与特色之路
在这个信息爆炸的时代,我们很容易陷入“千篇一律”的漩涡中,无论是生活方式还是兴趣爱好,似乎都趋向于某种“流行”或“热门”。然而,真正的个性与魅力,往往来源于那些不为大众所知的小众爱好。今天,我想和…...
GitHub使用教程(小白版)
看一百篇文章不如自己写一篇 第一步:注册和安装 注册GitHub账号 访问 GitHub官网。点击右上角的 "Sign up" 按钮。按照提示输入你的邮箱、创建用户名和密码,完成注册。 安装Git 访问 Git官网。下载并安装适用于你操作系统的Git。安装…...

深度解析SD-WAN在企业组网中的应用场景
在现代企业快速发展的网络环境中,SD-WAN技术不仅是实现企业各站点间高效连接的关键,也是满足不同站点对互联网、SaaS云应用和公有云等多种业务需求的理想选择。本文将从企业的WAN业务需求出发,对SD-WAN的组网场景进行全面解析,涵盖…...
【INTEL(ALTERA)】Eclipse Nios II SBT 无法从模板创建新应用程序和 BSP
目录 说明 解决方法 说明 您应该能够创建新的应用程序和 BSP 模板包含以下步骤: 选择 Nios II应用程序和 BSP 来自模板。选择您的.sopcinfo 文件并选择模板。从您的工作区单击 选择现有的 BSP 项目。单击 创建。选择所需的 BSP 选项。单击 完成。 但是…...

Vue_cli搭建过程项目创建
概述 vue-cli 官方提供的一个脚手架,用于快速生成一个 vue 的项目模板;预先定义 好的目录结构及基础代码,就好比咱们在创建 Maven 项目时可以选择创建一个 骨架项目,这个骨架项目就是脚手架,我们的开发更加的快速&am…...

面试题4:POST 比 GET 安全?
不是。HTTP就没有加密功能。 我们知道 GET一般将参数放到URL的查询字符串中,如果是实现登录页面,我们的用户名和密码就直接显示到浏览器的地址栏中了,此时就会轻易的被他人获取账号密码,很不安全。而POST会把参数放到 body 里&am…...

Github生成Personal access tokens及在git中使用
目录 生成Token 使用Token-手工修改 使用Token-自动 生成Token 登录GitHub,在GitHub右上角点击个人资料头像,点击Settings → Developer Settings → Personal access tokens (classic)。 在界面上选择点击【Generate new token】,填写如…...

【BUG记录】条件查询没有查询结果 || MybatisPlus打印查询语句
结论 先说结论,查询没有结果,可能是数据库连接,数据问题之类,最有可能的根本原因是查询语句问题,需要想办法检查查询语句,使用mybatisPlus等自动生成查询语句的框架不能直接看语句,可以依靠日志…...

【C#】找不到属性集方法。get只读属性用了反射设置setValue肯定报错
欢迎来到《小5讲堂》 这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。 温馨提示:博主能力有限,理解水平有限,若有不对之处望指正! 背景 找不到属性集方法。get只读属性用了反射设置setValue肯定报错 报错…...

探索ChatGPT在程序员日常工作的多种应用
引言 在现代科技迅猛发展的今天,人工智能的应用已经深入到我们生活和工作的各个方面。作为程序员,我们时常面临大量繁杂的任务,从代码编写、错误调试到项目管理和团队协作,每一项都需要花费大量的时间和精力。近年来,…...

算法与数据结构——时间复杂度详解与示例(C#,C++)
文章目录 1. 算法与数据结构概述2. 时间复杂度基本概念3. 时间复杂度分析方法4. 不同数据结构的时间复杂度示例5. 如何通过算法优化来提高时间复杂度6. C#中的时间复杂度示例7. 总结 算法与数据结构是计算机科学的核心,它们共同决定了程序的性能和效率。在实际开发中…...
面试题3:GET 和 POST 有什么区别?
[!]高频面试题。 GET 和 POST 没有本质区别,可以进行相互代替。 1、GET语义:“从服务器获取数据”;POST语义:“往服务器上提交数据”。[设计初衷,不一定要遵守] 2、发请求时,给服务器传递的数据ÿ…...
探索QCS6490目标检测AI应用开发(三):模型推理
作为《探索QCS6490目标检测AI应用开发》文章,紧接上一期,我们介绍如何在应用程序中介绍如何使用解码后的视频帧结合Yolov8n模型推理。 高通 Qualcomm AI Engine Direct 是一套能够针对高通AI应用加速的软件SDK,更多的内容可以访问:…...
C# 静态类中构造、字段和属性等的执行顺序,含有单例模式分析
C# 静态类时我们实战项目开发中用的非常多的。有些时候可能他的执行顺序并非如我们认为的那样,那就快速来看一下吧! 在C#中,静态类的构造函数是在第一次访问该类的任何成员时执行的。静态构造函数是不可继承的,并且在访问静态类的…...

c++设计模式之一创建型模式
1、创建型模式(常见的设计模式) Factory 模式(工厂模式,被实例化的子类) 在面向对象系统设计中经常可以遇到以下的两类问题: 下面是第一类问题和代码示例:我们经常会抽象出一些类的公共接口以…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...