DLedgerServer消息写入

news2024/12/22 12:01:30

下面以一个demo来理解消息DLedger消息写入和同步其他Peers的流程

    /**
     * 创建DLedgerServer
     */
	public DLedgerServer create(String selfId, String peers, String leaderId) {
        DLedgerConfig dLedgerConfig = new DLedgerConfig();
        dLedgerConfig.setMappedFileSizeForEntryData(1024);
        dLedgerConfig.setEnableLeaderElector(false);
        dLedgerConfig.setEnableDiskForceClean(false);
        dLedgerConfig.setStoreType(DLedgerConfig.FILE);
        dLedgerConfig.setPeers(peers);
        dLedgerConfig.setSelfId(selfId);
        dLedgerConfig.setGroup("test");
        DLedgerServer dLedgerServer = new DLedgerServer(dLedgerConfig);
        MemberState memberState = dLedgerServer.getMemberState();
        memberState.setCurrTermForTest(0);
        if (selfId.equals(leaderId)) {
            memberState.changeToLeader(0);
        } else {
            memberState.changeToFollower(0, leaderId);
        }
        dLedgerServer.startup();
        return dLedgerServer;
    }

    private String joinPeers(List<String> nodes, String s) {
        StringBuilder builder = new StringBuilder();
        for (String node : nodes) {
            builder.append(node).append(s);
        }
        builder.deleteCharAt(builder.length() - 1);
        String join = builder.toString();
        return join;
    }

    @Test
    public void appendTest() throws Exception {
    	//创建一个两peers的DLedgerServer,三个不利于debug代码
        List<String> nodes = Arrays.asList(
                "n1-localhost:1001",
                "n2-localhost:1002"
        );
        String peers = joinPeers(nodes, ";");
        DLedgerServer server1 = create("n1", peers, "n1");
        DLedgerServer server2 = create("n2", peers, "n1");
        Thread.sleep(1000L);
    	//保存一个消息并同步另一个peer
        AppendEntryRequest request = new AppendEntryRequest();
        request.setRemoteId(server1.getMemberState().getLeaderId());
        request.setGroup("test");
        request.setBody(new byte[500]);
        CompletableFuture<AppendEntryResponse> handleAppend = server1.handleAppend(request);
        AppendEntryResponse response = handleAppend.get();
        System.out.println(String.format("resp: index=%s, pos=%s", response.getIndex(), response.getPos()));
    }

handleAppend

RocketMQ的handleAppend流程是RocketMQ中与消息追加(Append)相关的一个重要流程,它涉及将生产者发送的消息追加到Broker的存储文件中。
方法里isPendingFull判断当前等待同步到peer的消息数是不是过多,默认是10000个。太多会导致系统内消息的堆积。
当消息不满时,会调用dLedgerStore.appendAsLeader先写入本地的存储。

    public CompletableFuture<AppendEntryResponse> handleAppend(AppendEntryRequest request) throws IOException {
			PreConditions.check(memberState.getSelfId().equals(request.getRemoteId()), DLedgerResponseCode.UNKNOWN_MEMBER, "%s != %s", request.getRemoteId(), memberState.getSelfId());
            PreConditions.check(memberState.getGroup().equals(request.getGroup()), DLedgerResponseCode.UNKNOWN_GROUP, "%s != %s", request.getGroup(), memberState.getGroup());
            PreConditions.check(memberState.isLeader(), DLedgerResponseCode.NOT_LEADER);
            PreConditions.check(memberState.getTransferee() == null, DLedgerResponseCode.LEADER_TRANSFERRING);
            long currTerm = memberState.currTerm();
            if (dLedgerEntryPusher.isPendingFull(currTerm)) {
                //pend请求满时
                AppendEntryResponse appendEntryResponse = new AppendEntryResponse();
                appendEntryResponse.setGroup(memberState.getGroup());
                appendEntryResponse.setCode(DLedgerResponseCode.LEADER_PENDING_FULL.getCode());
                appendEntryResponse.setTerm(currTerm);
                appendEntryResponse.setLeaderId(memberState.getSelfId());
                return AppendFuture.newCompletedFuture(-1, appendEntryResponse);
            } else {
                //pend请求不满时
                DLedgerEntry dLedgerEntry = new DLedgerEntry();
                dLedgerEntry.setBody(request.getBody());
                //loader本地保存日志
                DLedgerEntry resEntry = dLedgerStore.appendAsLeader(dLedgerEntry);
                //waitAck加入pend请求等待返回
                return dLedgerEntryPusher.waitAck(resEntry, false);
            }
    }

appendAsLeader

appendAsLeader和下文的appendAsFollower都是写入本地存储的详细代码,包含写消息和写索引。
dataFileList和CommitLog一样,设计上是一个可以不停追加写入的消息队列。当然dataFileList里写入的时候由于每个消息的大小不一样,所以在通过index的方式查询的时候只能一个个消息的遍历。为了提升查询的速度要需要使用到indexFileList,记录了每一个消息在dataFileList中的index和消息size,每一个indexBuffer的长度都是固定的,由magic(4)+pos(8)+size(4)+index(8)+term(8)共32个字节组成。
在查询时只要知道消息的index直接定位indexBuffer,再从indexBuffer中取出post、size,dataFileList就能读取到消息。

    @Override
    public DLedgerEntry appendAsLeader(DLedgerEntry entry) {
        PreConditions.check(memberState.isLeader(), DLedgerResponseCode.NOT_LEADER);
        PreConditions.check(!isDiskFull, DLedgerResponseCode.DISK_FULL);
        ByteBuffer dataBuffer = localEntryBuffer.get();
        ByteBuffer indexBuffer = localIndexBuffer.get();
        DLedgerEntryCoder.encode(entry, dataBuffer);
        int entrySize = dataBuffer.remaining();
        synchronized (memberState) {
            PreConditions.check(memberState.isLeader(), DLedgerResponseCode.NOT_LEADER, null);
            PreConditions.check(memberState.getTransferee() == null, DLedgerResponseCode.LEADER_TRANSFERRING, null);
            //生成日志的序号
            long nextIndex = ledgerEndIndex + 1;
            entry.setIndex(nextIndex);
            entry.setTerm(memberState.currTerm());
            entry.setMagic(CURRENT_MAGIC);
            DLedgerEntryCoder.setIndexTerm(dataBuffer, nextIndex, memberState.currTerm(), CURRENT_MAGIC);
            long prePos = dataFileList.preAppend(dataBuffer.remaining());
            entry.setPos(prePos);
            PreConditions.check(prePos != -1, DLedgerResponseCode.DISK_ERROR, null);
            DLedgerEntryCoder.setPos(dataBuffer, prePos);
            for (AppendHook writeHook : appendHooks) {
                writeHook.doHook(entry, dataBuffer.slice(), DLedgerEntry.BODY_OFFSET);
            }
            //写日志
            long dataPos = dataFileList.append(dataBuffer.array(), 0, dataBuffer.remaining());
            PreConditions.check(dataPos != -1, DLedgerResponseCode.DISK_ERROR, null);
            PreConditions.check(dataPos == prePos, DLedgerResponseCode.DISK_ERROR, null);
            DLedgerEntryCoder.encodeIndex(dataPos, entrySize, CURRENT_MAGIC, nextIndex, memberState.currTerm(), indexBuffer);
            //写索引
            long indexPos = indexFileList.append(indexBuffer.array(), 0, indexBuffer.remaining(), false);
            PreConditions.check(indexPos == entry.getIndex() * INDEX_UNIT_SIZE, DLedgerResponseCode.DISK_ERROR, null);
            if (logger.isDebugEnabled()) {
                logger.info("[{}] Append as Leader {} {}", memberState.getSelfId(), entry.getIndex(), entry.getBody().length);
            }
            ledgerEndIndex++;
            ledgerEndTerm = memberState.currTerm();
            if (ledgerBeginIndex == -1) {
                ledgerBeginIndex = ledgerEndIndex;
            }
            updateLedgerEndIndexAndTerm();
            return entry;
        }
    }

waitAck

leader写入后还需要同步到各个peers上,通过waitAck方法等待同步完成。waitAck是异步等待,创建CompletableFuture加入pendingAppendResponsesByTerm后异步监听同步结束。
pendingAppendResponsesByTerm为一个双层的Map,key为term、index,当同步完成后会从pendingAppendResponsesByTerm中通过term、index取出CompletableFuture,调用CompletableFuture.complete返回结果。

public CompletableFuture<AppendEntryResponse> waitAck(DLedgerEntry entry, boolean isBatchWait) {
    updatePeerWaterMark(entry.getTerm(), memberState.getSelfId(), entry.getIndex());
    checkTermForPendingMap(entry.getTerm(), "waitAck");
    AppendFuture<AppendEntryResponse> future;
    if (isBatchWait) {
        future = new BatchAppendFuture<>(dLedgerConfig.getMaxWaitAckTimeMs());
    } else {
        future = new AppendFuture<>(dLedgerConfig.getMaxWaitAckTimeMs());
    }
    future.setPos(entry.getPos());
    //加入pendingAppendResponsesByTerm异步等待follower写入完成
    CompletableFuture<AppendEntryResponse> old = pendingAppendResponsesByTerm.get(entry.getTerm()).put(entry.getIndex(), future);
    if (old != null) {
        logger.warn("[MONITOR] get old wait at index={}", entry.getIndex());
    }
    return future;
}

为什么是双层的Map,第一层为term,如果leader宕机触发重新选举term会+1,而老的term就全部移除了。
QuorumAckChecker

public void doWork() {
        .....
        long currTerm = memberState.currTerm();
        checkTermForPendingMap(currTerm, "QuorumAckChecker");
        checkTermForWaterMark(currTerm, "QuorumAckChecker");
        if (pendingAppendResponsesByTerm.size() > 1) {
            for (Long term : pendingAppendResponsesByTerm.keySet()) {
                if (term == currTerm) {
                    continue;
                }
                //当term不同时
                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);
            }
        }
    	......
}
            

QuorumAckChecker

pendingAppendResponsesByTerm在QuorumAckChecker线程执行,waitAck返回future异步等待
QuorumAckChecker有一个work方法不停的检查pendingAppendResponsesByTerm中有没有完成的future

    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);
        }

        @Override
        public void doWork() {
            ...
        }
    }

doWork最重要的部分如下

                //peerWaterMarksByTerm保存了每个peer已写入的index序号
                Map<String, Long> peerWaterMarks = peerWaterMarksByTerm.get(currTerm);
                List<Long> sortedWaterMarks = peerWaterMarks.values()
                        .stream()
                        .sorted(Comparator.reverseOrder())
                        .collect(Collectors.toList());
                //有半数peer写入完成
                long quorumIndex = sortedWaterMarks.get(sortedWaterMarks.size() / 2);
                final Optional<StateMachineCaller> fsmCaller = DLedgerEntryPusher.this.fsmCaller;
                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()) {
                            //回复handleAppend写入成功
                            AppendEntryResponse response = new AppendEntryResponse();
                            response.setGroup(memberState.getGroup());
                            response.setTerm(currTerm);
                            response.setIndex(i);
                            response.setLeaderId(memberState.getSelfId());
                            response.setPos(((AppendFuture) future).getPos());
                            System.out.println(String.format("complete: index=%s, pos=%s", response.getIndex(), response.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;

EntryDispatcher

peerWaterMarksByTerm在EntryDispatcher线程中同步给follower并调用updatePeerWaterMark更新
DLedgerServer启动时会根据peer的个数为每个peer创建一个EntryDispatcher用于同步消息

    public DLedgerEntryPusher(DLedgerConfig dLedgerConfig, MemberState memberState, DLedgerStore dLedgerStore,
                              DLedgerRpcService dLedgerRpcService) {
        this.dLedgerConfig = dLedgerConfig;
        this.memberState = memberState;
        this.dLedgerStore = dLedgerStore;
        this.dLedgerRpcService = dLedgerRpcService;
        //没peer创建一个EntryDispatcher
        for (String peer : memberState.getPeerMap().keySet()) {
            if (!peer.equals(memberState.getSelfId())) {
                dispatcherMap.put(peer, new EntryDispatcher(peer, logger));
            }
        }
        this.entryHandler = new EntryHandler(logger);
        this.quorumAckChecker = new QuorumAckChecker(logger);
        this.fsmCaller = Optional.empty();
    }
        private void doAppend() throws Exception {
            while (true) {
                if (!checkAndFreshState()) {
                    break;
                }
                if (type.get() != PushEntryRequest.Type.APPEND) {
                    break;
                }
                if (writeIndex > dLedgerStore.getLedgerEndIndex()) {
                    doCommit();
                    doCheckAppendResponse();
                    break;
                }
                if (pendingMap.size() >= maxPendingSize || DLedgerUtils.elapsed(lastCheckLeakTimeMs) > 1000) {
                    long peerWaterMark = getPeerWaterMark(term, peerId);
                    for (Long index : pendingMap.keySet()) {
                        if (index < peerWaterMark) {
                            pendingMap.remove(index);
                        }
                    }
                    lastCheckLeakTimeMs = System.currentTimeMillis();
                }
                if (pendingMap.size() >= maxPendingSize) {
                    //处理同步失败后重试
                    doCheckAppendResponse();
                    break;
                }
                //复制消息到follower
                doAppendInner(writeIndex);
                writeIndex++;
            }
        }
        private void doAppendInner(long index) throws Exception {
            //根据index查询loader写入的entry数据
            DLedgerEntry entry = getDLedgerEntryForAppend(index);
            if (null == entry) {
                return;
            }
            checkQuotaAndWait(entry);
            PushEntryRequest request = buildPushRequest(entry, PushEntryRequest.Type.APPEND);
            //发送复制请求
            CompletableFuture<PushEntryResponse> responseFuture = dLedgerRpcService.push(request);
            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());
                            //更新peerWaterMarksByTerm
                            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();
        }

NettyRpcServer接收请求后调用handlePush处理

        public CompletableFuture<PushEntryResponse> handlePush(PushEntryRequest request) throws Exception {
            //The timeout should smaller than the remoting layer's request timeout
            CompletableFuture<PushEntryResponse> future = new TimeoutFuture<>(1000);
            switch (request.getType()) {
                case APPEND:
                    if (request.isBatch()) {
                        PreConditions.check(request.getBatchEntry() != null && request.getCount() > 0, DLedgerResponseCode.UNEXPECTED_ARGUMENT);
                    } else {
                        PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);
                    }
                    long index = request.getFirstEntryIndex();
                    //请求写入writeRequestMap
                    Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> old = writeRequestMap.putIfAbsent(index, new Pair<>(request, future));
                    if (old != null) {
                        logger.warn("[MONITOR]The index {} has already existed with {} and curr is {}", index, old.getKey().baseInfo(), request.baseInfo());
                        future.complete(buildResponse(request, DLedgerResponseCode.REPEATED_PUSH.getCode()));
                    }
                    break;
                case COMMIT:
                    compareOrTruncateRequests.put(new Pair<>(request, future));
                    break;
                case COMPARE:
                case TRUNCATE:
                    PreConditions.check(request.getEntry() != null, DLedgerResponseCode.UNEXPECTED_ARGUMENT);
                    writeRequestMap.clear();
                    compareOrTruncateRequests.put(new Pair<>(request, future));
                    break;
                default:
                    logger.error("[BUG]Unknown type {} from {}", request.getType(), request.baseInfo());
                    future.complete(buildResponse(request, DLedgerResponseCode.UNEXPECTED_ARGUMENT.getCode()));
                    break;
            }
            wakeup();
            return future;
        }

EntryHandler

dLedgerRpcService.push发送Append消息,在follower节点EntryHandler接收处理

        @Override
        public void doWork() {
            try {
                if (!memberState.isFollower()) {
                    waitForRunning(1);
                    return;
                }
                if (compareOrTruncateRequests.peek() != null) {
                    Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = compareOrTruncateRequests.poll();
                    PreConditions.check(pair != null, DLedgerResponseCode.UNKNOWN);
                    switch (pair.getKey().getType()) {
                        case TRUNCATE:
                            handleDoTruncate(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
                            break;
                        case COMPARE:
                            handleDoCompare(pair.getKey().getEntry().getIndex(), pair.getKey(), pair.getValue());
                            break;
                        case COMMIT:
                            handleDoCommit(pair.getKey().getCommitIndex(), pair.getKey(), pair.getValue());
                            break;
                        default:
                            break;
                    }
                } else {
                    long nextIndex = dLedgerStore.getLedgerEndIndex() + 1;
                    Pair<PushEntryRequest, CompletableFuture<PushEntryResponse>> pair = writeRequestMap.remove(nextIndex);
                    if (pair == null) {
                        checkAbnormalFuture(dLedgerStore.getLedgerEndIndex());
                        waitForRunning(1);
                        return;
                    }
                    PushEntryRequest request = pair.getKey();
                    if (request.isBatch()) {
                        handleDoBatchAppend(nextIndex, request, pair.getValue());
                    } else {
                        //处理写入请求
                        handleDoAppend(nextIndex, request, pair.getValue());
                    }
                }
            } catch (Throwable t) {
                DLedgerEntryPusher.logger.error("Error in {}", getName(), t);
                DLedgerUtils.sleep(100);
            }
        }
    }

appendAsFollower

appendAsFollower的流程与appendAsLeader的流程类型

        private void handleDoAppend(long writeIndex, PushEntryRequest request,
                                    CompletableFuture<PushEntryResponse> future) {
            try {
                PreConditions.check(writeIndex == request.getEntry().getIndex(), DLedgerResponseCode.INCONSISTENT_STATE);
            	//写入日志
                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()));
                System.out.println(String.format("handleDoAppend: thread=%s, self=%s, nextIndex=%s, indexPos=%s",
                        Thread.currentThread().getName(), memberState.getSelfId(), entry.getIndex(), entry.getPos()));
                updateCommittedIndex(request.getTerm(), request.getCommitIndex());
            } catch (Throwable t) {
                logger.error("[HandleDoWrite] writeIndex={}", writeIndex, t);
                future.complete(buildResponse(request, DLedgerResponseCode.INCONSISTENT_STATE.getCode()));
            }
        }

流程图


1.EntryDispatcher线程: 处理向follower同步数据
2.QuorumAckChecker线程: 处理半数peer写入成功返回
3.EntryHandler线程: 接收EntryDispatcher发送的同步请求写入数据
4.WaterMark(peerWaterMarksByTerm):当前term下peer同步的index序号
在代码中以Map的方式存储
Map<Long, ConcurrentMap<String, Long>> peerWaterMarksByTerm

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

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

相关文章

Wt库的下载器程序

#include <stdio.h> #include <string.h> #include <stdlib.h> #include <microhttpd.h> // 设置服务器的地址和端口号 const char *proxy_host ""; const int proxy_port 8000; // 下载的文件路径 const char *filename "xiuxiu.…

电脑桌面文件不见了怎么恢复?4个方法!(详细步骤分享)

“这下可怎么办呀&#xff1f;我有些比较重要的文件保存在桌面&#xff0c;现在这些文件都莫名奇妙的丢失了&#xff0c;我应该怎么将它们恢复呢&#xff1f;快帮帮我吧&#xff01;” 在日常使用电脑时&#xff0c;很多电脑用户为了方便&#xff0c;可能会随手将一些文件直接保…

微信视频怎么录屏?超简单教程,一学就会!

“微信视频的时候怎么录屏呀&#xff1f;和朋友打微信视频&#xff0c;她每次都偷偷录我的丑照&#xff0c;我也想把她录下来&#xff0c;但是不会怎么操作&#xff0c;求求大家教教我&#xff01;” 微信作为全球最大的社交媒体平台之一&#xff0c;承载了无数珍贵的瞬间和回…

iPhone强制重启教程来了!(已解决)

强制重启是解决苹果手机系统故障问题的首选操作&#xff0c;是通过特殊组合按键来强制关机并重新启动设备的操作。可以用来应对系统崩溃、设备无响应、白苹果、死机、闪退、莫名其妙黑屏等情况。 那么大家知道该如何将iPhone手机强制重启吗&#xff1f;iphone强制重启的方法是…

福建厦门航空飞机零部件检测3D扫描测量尺寸偏差-CASAIM中科广电

航空航天是一个创新型发展国家的尖端命脉&#xff0c;代表着一个国家科学技术的先进水平。在航空航天工业的发展和组成领域中&#xff0c;对于在制造业中的航空航天产品零部件精度要求十分严苛&#xff0c;从前期的设计、中期建造、后期维修检测&#xff0c;任何一个环节、任何…

Mybatis一对多查询,分页显示问题解决方案

原文&#xff1a;Mybatis一对多查询&#xff0c;分页显示问题解决方案_mybatis一对多分页问题-CSDN博客 在我们的开发中也许是遇到最多的功能&#xff0c;一张表的分页&#xff0c;多张表一对一功能的分页相信大家写来都是得心应手&#xff0c;但是在一对多分页查询的时候大家…

【Ubuntu18.04】激光雷达与相机联合标定(Livox+HIKROBOT)(一)相机内参标定

LivoxHIKROBOT联合标定——相机内参标定 引言1 海康机器人HIKROBOT SDK二次开发并封装ROS1.1 介绍1.2 安装MVS SDK1.3 封装ROS packge 2 览沃Livox SDK二次开发并封装ROS3 相机雷达联合标定——相机内参标定3.1 环境配置3.1.1 安装依赖——PCL 安装3.1.2 安装依赖——Eigen 安装…

【表面缺陷检测】钢轨表面缺陷检测数据集介绍(2类,含xml标签文件)

一、介绍 钢轨表面缺陷检测是指通过使用各种技术手段和设备&#xff0c;对钢轨表面进行检查和测量&#xff0c;以确定是否存在裂纹、掉块、剥离、锈蚀等缺陷的过程。这些缺陷可能会对铁路运输的安全和稳定性产生影响&#xff0c;因此及时进行检测和修复非常重要。钢轨表面缺陷…

GaussDB SQL基础语法示例-BOOLEAN表达式

目录 一、前言 二、GaussDB SQL 中的BOOLEAN表达式介绍 1、概念 2、组成 3、语法示例 三、在GaussDB SQL中的基础应用 1、示例1&#xff0c;使用比较运算符 2、示例2&#xff0c;使用逻辑运算符 3、示例3&#xff0c;使用IS NOT NULL运算符 4、示例4&#xff0c;使用…

vscode安装包下载——vscode的下载速度慢问题两种解决方法

1.vscode下载&#xff1a; 1.首先我们去官网下载vccode&#xff0c;下载过程非常慢&#xff1a; 官网链接&#xff1a; https://code.visualstudio.com/ 2.解决办法1 这是因为国外镜像地址下载慢的原因&#xff0c;此时需要去国内镜像地址进行下载&#xff0c;复制下载链接&…

【Andriod】使用adb命令安装和卸载apk的通用python脚本

文章目录 1.前言2.连接设备3.从本机通过adb安装apk4.从本机通过adb卸载apk 1.前言 如不会使用adb请看之前的文章 【Andriod】adb调试安卓手机时连接真机或模拟器的3种方法&#xff0c;你知道么&#xff1f; 2.连接设备 import os # python标准库中的os模块""&qu…

互动直播UI设置 之 主播UI

目录 一、普通模式下&#xff0c;布局选项 1、布局按钮 1&#xff09;、点击布局按钮 2&#xff09;、选择哪种布局后&#xff0c;主进程通过WM_COPYDATA 发送信息 2、duilib样式布局文件 1&#xff09;、主画面 2&#xff09;、连麦者画面 3、主画面自动调整宽度 1&a…

UnrealSynth - 基于虚幻引擎的YOLO合成数据生成器

UnrealSynth虚幻合成数据生成器利用虚幻引擎的实时渲染能力搭建逼真的三维场景&#xff0c;为YOLO等AI模型的训练提供自动生成的图像和标注数据&#xff0c;官方下载地址&#xff1a;UnrealSynth虚幻合成数据生成器。 UnrealSynth生成的合成数据可用于深度学习模型的训练和验证…

18.3 NPCAP 构建中间人攻击

ARP欺骗&#xff08;ARP Spoofing&#xff09;是一种网络攻击手段&#xff0c;其目的是通过欺骗目标主机来实现网络攻击。ARP协议是一种用于获取MAC地址的协议&#xff0c;因此欺骗者可以使用ARP欺骗来迫使其目标主机将网络流量发送到攻击者控制的设备上&#xff0c;从而实现网…

error LNK2019: 无法解析的外部符号

文章目录 1 问题2 出现该问题的原因和解决方法2.1 原因&#xff1a;2.3 解决方法&#xff1a;需要查看一下项目的属性配置是否正确&#xff1a; 3 其他可能得原因&#xff0c;但是本项目中没有出现 1 问题 在测试base64代码的时候&#xff0c;VS2022提示我错误如下&#xff1a…

Julia数值计算初步

文章目录 复数系统运算符三角函数指数、对数、取整 Julia系列&#xff1a;编程初步&#x1f525;数组 Julia作为主打数值计算的编程语言&#xff0c;对一些常用的计算函数提供了非常细致的支持&#xff0c;十分人性化&#xff0c;体验之后爱不释手。 复数系统 在Juli中&…

笔记本怎么录屏?这3个方法请你收好

在现代教育、工作和演示中&#xff0c;屏幕录制成为了无可替代的工具。它使我们能够捕捉电脑屏幕上的一切&#xff0c;从PPT演示到教程制作&#xff0c;再到技术支持。因此&#xff0c;选择合适的录屏工具显得尤为重要。本文将详细介绍笔记本怎么录屏的3个方法&#xff0c;这些…

软件绘制 硬件加速绘制 【DisplayList RenderNode】

Android4.0以后&#xff0c;系统默认开启硬件加速来渲染视图 异同点 共同点 两者都是从SF获取一块内存&#xff0c;绘制都是在APP端&#xff0c;绘制好后都是通知SF去进行合成图层 真正的区别 真正的区别&#xff1a;绘制是通过CPU还是GPU完成的视图绘制。 对应区别在代码中的体…

SpringBoot整合Gateway 的Demo(附源码)

源码&#xff0c;可直接下载 Gateway模块 Gateway 的父pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sc…

学习嵌入式可以胜任哪一些行业?

今日话题&#xff0c;学习嵌入式可以胜任哪一些行业&#xff1f;学习嵌入式开发可以胜任许多行业&#xff0c;因为嵌入式技术在各个领域都有广泛的应用。嵌入式系统用于医疗设备、患者监测系统、药物分发设备等。智能手机、智能家居设备、游戏机等消费电子产品都包含嵌入式系统…