当前位置: 首页 > article >正文

用go从零构建写一个RPC(3)--异步调用+多路复用实现

在前两个版本中,我们实现了基础的客户端-服务端通信、连接池、序列化等关键模块。为了进一步提升吞吐量和并发性能,本版本新增了 异步发送机制多路复用支持,旨在减少资源消耗、提升连接利用率。
代码地址:https://github.com/karatttt/MyRPC

版本三新增特性

异步发送机制实现

背景:
在同步RPC调用中,客户端每发送一次请求都需阻塞等待响应,这在网络抖动或响应较慢时会严重降低系统吞吐量。因此,本版本引入了 异步任务模型,支持超时重试、指数退避、完成回调等能力,确保在客户端请求失败后可以自动重试、不中断主逻辑。

实现思路:

  • 实际上异步回调的功能很好实现,只需要将回调方法传入内部,当内部状态为成功或者完成的时候调用该callback方法即可。
  • 而异步超时失败重试的机制实际上是让客户端的发送请求交由另一个协程来做,客户端可以先执行其他的逻辑再阻塞等待future的结果,或者设置一个回调方法,或者不关心回复。异步发送实际上就是牺牲了可靠性,而重试是为了尽量提高这个可靠性。超时重试这个可以通过在协程内通过计时器重试,如果超时则在同一个协程中再进行一次发送,直到重试到大于最大重试次数。但是这样会导致等待重试的协程数量太多,对于某一时间段网络出现抖动的情况,出现了大量的重试,就会导致协程数剧增的情况。
  • 借鉴了RocketMQ的异步发送的机制,采用了一个协程统一管理需要重试的任务,并用一个延时队列来排序处理任务

Client的变动
为了区分同步发送,为异步增加了异步的proxy和异步的send方法

// 创建客户端代理
func NewHelloAsyncClientProxy(opts ...client.Option) HelloAsyncClientProxy {return &HelloAsyncClientProxyImpl{client: client.DefaultClient,opts:   opts,}
}
// 实现HelloAsync方法
func (c *HelloAsyncClientProxyImpl) HelloAsync(ctx context.Context, req *HelloRequest, opts ...client.Option) (*internel.Future, *common.RPCError) {msg := internel.NewMsg()msg.WithServiceName("helloworld")msg.WithMethodName("Hello")ctx = context.WithValue(ctx, internel.ContextMsgKey, msg)rsp := &HelloReply{}// 这里需要将opts添加前面newProxy时传入的optsnewOpts := append(c.opts, opts...)return c.client.InvokeAsync(ctx, req, rsp, newOpts...)
}
  • 这里是rpc.go中新增的代理以及实现方法,还没有体现异步发送的逻辑,接下来看InvokeAsync

InvokeAsync

func (c *client) InvokeAsync(ctx context.Context, reqBody interface{}, rspBody interface{}, opt ...Option) (*internel.Future, *common.RPCError) {future := internel.NewFuture()opts := DefaultOptionsfor _, o := range opt {o(opts)}go func() {var task *async.Taskif opts.Timeout > 0 {// 有超时时间的情况下,无论是否进行重试,将任务提交给全局管理器ctx, msg := internel.GetMessage(ctx)task = &async.Task{MethodName:  msg.GetMethodName(),Request:     reqBody,MaxRetries:  opts.RetryTimes,Timeout:     opts.Timeout,ExecuteFunc: c.makeRetryFunc(ctx, reqBody, rspBody, opts),OnComplete: func(err error) {// 最终结果回调到原始Futureif err != nil {future.SetResult(nil, &common.RPCError{Code:    common.ErrCodeRetryFailed,Message: err.Error(),})} else {future.SetResult(rspBody, nil)}},}// 提交任务到全局管理器task.Status = async.TaskStatusPendingfuture.Task = taskasync.GetGlobalTaskManager().AddTask(task)// 执行发送逻辑err := opts.ClientTransport.Send(ctx, reqBody, rspBody, opts.ClientTransportOption)if err == nil {future.SetResult(rspBody, nil)}} else {// 无超时时间的情况下,错误的话直接返回err := opts.ClientTransport.Send(ctx, reqBody, rspBody, opts.ClientTransportOption)if err == nil {future.SetResult(rspBody, nil)} else {future.SetResult(nil, &common.RPCError{Code:    common.ErrCodeClient,Message: err.Error(),})}}}()return future, nil
}
  • 我们先看看Future结构,再去理解上面的代码:
	type Future struct {mu        sync.Mutexdone      chan struct{}result    interface{}err       *common.RPCErrorcallbacks []func(interface{}, *common.RPCError)Task      *async.Task // 关联的异步任务}// SetResult 设置Future的结果func (f *Future) SetResult(result interface{}, err *common.RPCError) {f.mu.Lock()defer f.mu.Unlock()if f.isDone() {return}f.result = resultf.Task.Status = async.TaskStatusCompletedf.err = errclose(f.done)// 执行所有注册的回调for _, callback := range f.callbacks {callback(result, err)}}
  • 这个就是异步发送后返回的Future,result就是回包结果,callbacks就是客户端设置的回调方法,Task是后续添加到全局异步管理器的任务,后续再说
  • 而这个SetResult就是在得到结果后设置future的result,并且调用所有注册的回调方法,并置Task.Status = async.TaskStatusCompleted,这个关于task的我们后面再说
  • 接下来回到invokeAsync,对于没有设置超时时间的发送,我们直接在失败后返回客户端(客户端能够忍受异步的丢失,如果真的发生了长时间的阻塞,也不用担心这个协程不释放,因为我们的连接池会管理这个连接的生命周期),对于设置了超时时间的发送,我们需要在超时时间到达后进行重试,或者达到最大重试次数后进行失败反馈
  • 这里就做了一个全局的管理器,先创建一个Task将其添加到manager中,再进行消息的正常发送。

TaskManager

// Task 表示一个异步任务
type Task struct {MethodName  string             // 方法名Request      interface{}        // 请求参数RetryTimes   int                // 当前已重试次数MaxRetries   int                // 最大重试次数Timeout      time.Duration      // 单次任务超时时间NextRetryAt  time.Time         // 下次重试时间(用于堆排序)ExecuteFunc  func() error       // 重试时任务执行函数Status       TaskStatus        // 状态字段OnComplete   func(error)       // 最终完成回调mu           sync.Mutex // 保证状态变更的线程安全
}// 扫描循环(核心逻辑)
func (tm *TaskManager) scanLoop() {for {select {case <-tm.closeChan:returndefault:tm.processTasks()}}
}// 处理超时任务
func (tm *TaskManager) processTasks() {tm.mu.Lock()if tm.tasks.Len() == 0 {tm.mu.Unlock()// 无任务时休眠,直到被唤醒select {case <-tm.wakeChan:case <-time.After(10 * time.Second): // 防止长期阻塞}return}// 检查堆顶任务是否超时now := time.Now()task := (*tm.tasks)[0]if now.Before(task.NextRetryAt) {// 未超时,休眠到最近任务到期tm.mu.Unlock()time.Sleep(task.NextRetryAt.Sub(now))return}// 弹出超时任务task = heap.Pop(tm.tasks).(*Task)tm.mu.Unlock()// 执行重试逻辑go tm.retryTask(task)
}// 重试任务
func (tm *TaskManager) retryTask(task *Task) {task.mu.Lock()// 检查状态:如果任务已结束,直接返回,不用再次入队列if task.Status != TaskStatusPending {task.mu.Unlock()return}task.Status = TaskStatusRunning // 标记为执行中task.mu.Unlock()err := task.ExecuteFunc()if err == nil {task.OnComplete(nil)return}// 检查是否达到最大重试次数task.RetryTimes++if task.RetryTimes > task.MaxRetries {// 打印fmt.Println("request retry times exceed max retry times")task.OnComplete(err)return}// 计算下次重试时间(如指数退避)delay := time.Duration(math.Pow(2, float64(task.RetryTimes))) * time.Secondtask.NextRetryAt = time.Now().Add(delay) // 重新加入队列// 打印重试次数fmt.Println("request retry time : ", task.RetryTimes)tm.mu.Lock()heap.Push(tm.tasks, task)task.Status = TaskStatusPending // 恢复状态tm.mu.Unlock()tm.notifyScanner()
}
  • 以上是这个manager的关键代码,这个Task就是里面的元素,按照下一次重试时间排序放在manager的一个延时队列里面,优先处理目前需要重试的任务。task的ExecuteFunc我们在前面的方法中可以看到实际上就是retry发送,OnComplete就是将future的setResult使得客户端能得到反馈
  • 循环执行processTasks,对于堆顶任务进行retry
  • retry时先看这个task是不是已经执行成功了,是的话删除这个task,如果不是的话继续入队
  • 这样就可以保证只有一个协程在管理所有的超时任务,避免了每一个超时任务都需要一个协程来等待重试。

多路复用

背景:

  • 默认情况下,每个RPC调用使用一个连接,连接池虽然能缓解资源浪费,对于连接池中的每一个连接,实际上也是串行进行的,也就是说,如果前面的某一个连接处理时间太长,后续的请求只能等待该请求返回后才能复用该连接,也就是http1.1的队头阻塞问题。
  • 为此,引入 多路复用协议 —— 即在一个TCP连接内支持多个“逻辑流”,每个流由 RequestID 唯一标识,从而支持多个请求同时复用一条连接。

实现思路:
我们之前的frame结构如下:

header := FrameHeader{MagicNumber:    MagicNumber,Version:        Version,MessageType:    MessageTypeRequest,SequenceID:     sequenceID, ProtocolLength: uint32(len(protocolDataBytes)),BodyLength:     uint32(len(reqData)),}

实际上已经有了SequenceID这个字段,也就是说,我们可以通过这个SequenceID,来区分同一个连接中的不同的流,也就是说,客户端在同一个连接中,发送了不同的SequenceID的消息,服务端并发处理这些消息,并且保留这个SequenceID返回客户端,客户端的多个流识别这个SequenceID并读取结果

MuxConn(多路复用连接)结构

// 实现net.Conn接口的结构体,保证适配连接池的get和put
// 实际上也是一个连接,只是多了reqID从而可以派生出多个流,区分达到多路复用的目的
type MuxConn struct {conn         net.Conn                   // 原始连接pending      map[uint32]*pendingRequest // 每一个reqID(流)对应的等待通道closeChan    chan struct{}readerDone   chan struct{}writeLock    sync.MutexreqIDCounter uint64 // 分配递增的请求IDmu           sync.RWMutex
}
type pendingRequest struct {ch      chan MuxFrametimeout time.Time
}
func (mc *MuxConn) NextRequestID() uint64 {return atomic.AddUint64(&mc.reqIDCounter, 1)
}
  • 实际上这个MuxConn实现了net.Conn,也是一个连接,只是可以通过NextRequestID派生出多个流,并在这个conn上write特定reqID的请求
  • 可以看到pending这个结构,是一个map,k是reqID,v是一个ch,为什么要设计一个这样的map?因为我们可能同时存在多路并发,不同的客户端的对于同一个conn的请求,我们需要设计一个特有的ch来读取对应的reqID的响应是否到达,如果某一个reqID的响应到达了,发送到对应的ch,从而对应的客户端得到响应。如果多个流直接并发读取tcp的响应,必然会导致reqID乱序现象

connPool的改动
之前的连接池只是正常获取一个连接,当该连接处理完被归还后才置为空闲状态。而对于多路复用显然不是这个规则,对于正在使用的连接,若没有达到最大可以接受的流的量,我们仍然可以接受从池中返回这个连接并使用

对于之前的获取连接的逻辑,我们一次对于多路复用加入以下分支:

        // 1. 优先检查空闲连接if len(p.idleConns) > 0 {// 原逻辑。。。// 多路复用处理if p.isMux {if muxConn, exists := p.muxConns[conn]; exists {if p.streamCount[conn] < p.maxStreams {p.streamCount[conn]++MuxConn2SequenceIDMap[muxConn] = muxConn.NextRequestID()return muxConn, nil}}// 如果不是多路复用连接或已达最大流数,回退到普通连接}p.mu.Unlock()return &pooledConnWrapper{conn, p}, nil}// 2. 检查是否可以创建新连接if int(atomic.LoadInt32(&p.activeCount)) < p.maxActive {// 原逻辑。。。// 多路复用连接初始化if p.isMux {if p.muxConns == nil {p.muxConns = make(map[*PooledConn]*mutilpath.MuxConn)p.streamCount = make(map[*PooledConn]int)}muxConn := mutilpath.NewMuxConn(rawConn, 1000)p.muxConns[pooledConn] = muxConnp.streamCount[pooledConn] = 1 // 新连接默认1个流MuxConn2SequenceIDMap[muxConn] = muxConn.NextRequestID()return muxConn, nil}p.mu.Unlock()return &pooledConnWrapper{pooledConn, p}, nil}// 3. 新增情况:无空闲且活跃连接达到最大数,检查活跃连接的多路复用能力(仅在多路复用模式下)if p.isMux {for pc, muxConn := range p.muxConns {count := p.streamCount[pc]if count < p.maxStreams {p.streamCount[pc]++atomic.AddInt32(&p.activeCount, 1)pc.lastUsed = time.Now()MuxConn2SequenceIDMap[muxConn] = muxConn.NextRequestID()return p.muxConns[pc], nil}}}
  • 对于情况一,若是空闲连接当然直接使用,并增加流数量,并对该连接分配reqID,在MuxConn2SequenceIDMap结构中保存
  • 对于情况二,无空闲连接,但是活跃连接数未满,创建新连接,增加流数量,并对该连接分配reqID,在MuxConn2SequenceIDMap结构中保存
  • 对于情况三,无空闲连接且活跃连接数已经满,检查所有的活跃连接的流数量是否未满,并且返回未满的连接,分配新的流
  • 对于Put逻辑,对应的应是归还流,当某个连接的流为0时,该连接为空闲状态,不再阐述

Send方法改动
之前的方法只需要send中正常序列化和编解码就可以,客户端发送完请求就阻塞(或者异步)等待响应,这里的多路复用模式则是在write前注册一个pendingRequest,监听特定的channel

// mux模式下,通过ch阻塞等待相应的流回包muxConn, _ := conn.(*mutilpath.MuxConn)seqID := msg.GetSequenceID()ch := muxConn.RegisterPending(seqID)defer muxConn.UnregisterPending(seqID)// 写数据err = c.tcpWriteFrame(ctx, conn, framedata)if err != nil {return &common.RPCError{Code:    common.ErrCodeNetwork,Message: fmt.Sprintf("failed to write frame: %v", err),}}// 读响应select {case frame := <-ch:rspDataBuf = frame.Datacase <-ctx.Done():return &common.RPCError{Code:    common.ErrCodeNetwork,Message: fmt.Sprintf("failed to read frame: %v", err),}}
  • 而客户端收到响应,路由到对应reqID的channel的逻辑在这里:

func (mc *MuxConn) readLoop() {defer close(mc.readerDone)for {select {case <-mc.closeChan:returndefault:}frame, err := codec.ReadFrame(mc.conn)if err != nil {// 协议错误处理fmt.Println("读取帧错误:", err)break}mc.dispatchFrame(frame)}
}func (mc *MuxConn) dispatchFrame(frame []byte) {mc.mu.RLock()// 截取流序号sequenceID := binary.BigEndian.Uint32(frame[4:8])pr, exists := mc.pending[uint32(sequenceID)]mc.mu.RUnlock()frameStruct := MuxFrame{Data: frame,}if exists {select {case pr.ch <- frameStruct:// 成功发送到等待通道default:// 通道已满,丢弃帧fmt.Println("丢弃帧 %s:通道已满", frame)}} else {// 直接丢弃或打印日志fmt.Printf("收到未匹配的帧,sequenceID=%d,丢弃\n", sequenceID)}
}

总结

在已有基础通信、连接池与序列化机制之上,通过引入异步发送机制与多路复用技术进一步提升RPC系统的吞吐量与并发性能,使得系统更加健壮。多路复用实际上也是http2.0实现的能力,这里相当于完成了http2.0的任务。以后的版本可以考虑对于性能再进行优化,如网络框架的改进以及更高效的数据结构的使用

相关文章:

用go从零构建写一个RPC(3)--异步调用+多路复用实现

在前两个版本中&#xff0c;我们实现了基础的客户端-服务端通信、连接池、序列化等关键模块。为了进一步提升吞吐量和并发性能&#xff0c;本版本新增了 异步发送机制 和 多路复用支持&#xff0c;旨在减少资源消耗、提升连接利用率。 代码地址&#xff1a;https://github.com/…...

力扣395做题笔记

题目链接 力扣395 第一次尝试 class Solution {public int longestSubstring(String str, int k) {char[] s str.toCharArray();int n s.length;int[] cnts new int[256];int ans 0;for (int r 0, l 0; r < n; r ) { cnts[s[r]];if (cnts[s[r]] > k) { ans Mat…...

Python-numpy中常用的统计函数及转换函数

numpy中常用的统计函数 numpy中常用统计函数numpy普通统计函数忽略 NaN 值进行统计百分位数 numpy中形状转换函数重塑数组&#xff08;reshape&#xff09;展平数组&#xff08;flatten/ravel&#xff09;转置&#xff08;transpose/T&#xff09; 数据类型的转换使用astype()转…...

【C语言干货】free细节

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、为啥*phead free掉了之后&#xff0c;为啥下面还 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供 可以用&#xff1f; 前言参考 一、为…...

网络安全-等级保护(等保) 2-0 等级保护制度现行技术标准

################################################################################ 第二章&#xff1a;现行等保标准要求&#xff0c;通过表格方式详细拆分了等保的相关要求。 GB 17859-1999 计算机信息系统 安全保护等级划分准则【现行】 GB/T22240-2020 《信息安全技术 网…...

WebSocket(看这一篇就够了)

文章目录 WebSocket 基本概念什么是WebSocket?为什么需要 WebSocket&#xff1f;与 HTTP 协议的区别WebSocket协议的原理WebSocket工作流程WebSocket 数据帧结构和控制帧结构。JavaScript 中 WebSocket 对象的属性和方法&#xff0c;以及如何创建和连接 WebSocket。webSocket简…...

旧物回收小程序:让闲置焕发光彩,为生活增添价值

你是否常常为家中堆积如山的闲置物品而烦恼&#xff1f;那些曾经心爱的物品&#xff0c;如今却成了占据空间的“鸡肋”&#xff0c;丢弃可惜&#xff0c;留着又无处安放。别担心&#xff0c;一款旧物二手回收小程序将为你解决这一难题&#xff0c;让闲置物品重新焕发光彩&#…...

精益数据分析(73/126):黏性阶段的功能优先级法则——七问决策模型与风险控制

精益数据分析&#xff08;73/126&#xff09;&#xff1a;黏性阶段的功能优先级法则——七问决策模型与风险控制 在创业的黏性阶段&#xff0c;如何从海量的功能创意中筛选出真正能提升用户留存的关键改动&#xff1f;今天&#xff0c;我们结合《精益数据分析》中的“开发功能…...

React声明式编程(手动控制,大型项目,深度定制)与Vue响应式系统(自动优化,中小型项目,快速开发)区别

文章目录 React声明式与Vue响应式区别详解一、响应式机制原理对比1.1 Vue的响应式系统Vue响应式流程图Vue响应式代码示例 1.2 React的声明式更新React声明式流程图React声明式代码示例 二、更新触发逻辑差异2.1 Vue的自动更新Vue依赖收集机制 2.2 React的手动更新React Diff算法…...

数学建模MathAI智能体-2025电工杯A题实战

题目&#xff1a; 光伏电站发电功率日前预测问题 光伏发电是通过半导体材料的光电效应&#xff0c;将太阳能直接转化为电能的技术。光伏电站是由众多光伏发电单元组成的规模化发电设施。 光伏电站的发电功率主要由光伏板表面接收到的太阳辐射总量决定&#xff0c;不同季节太阳…...

跨平台游戏引擎 Axmol-2.6.0 发布

Axmol 2.6.0 版本是一个以错误修复和功能改进为主的次要LTS长期支持版本 &#x1f64f;感谢所有贡献者及财务赞助者&#xff1a;scorewarrior、peterkharitonov、duong、thienphuoc、bingsoo、asnagni、paulocoutinhox、DelinWorks 相对于2.5.0版本的重要变更&#xff1a; 通…...

C# Windows Forms应用程序-002

目录 项目结构 主类和命名空间 构造函数和析构函数 初始化组件 (InitializeComponent) 按钮点击事件处理程序 主程序入口点 项目截图&#xff1a; 完整代码&#xff1a; 项目结构 这个项目是一个简单的C# Windows Forms应用程序&#xff0c;获取指定文件的根信息…...

理解计算机系统_线程(八):并行

前言 以<深入理解计算机系统>(以下称“本书”)内容为基础&#xff0c;对程序的整个过程进行梳理。本书内容对整个计算机系统做了系统性导引,每部分内容都是单独的一门课.学习深度根据自己需要来定 引入 接续理解计算机系统_并发编程(10)_线程(七):基于预线程化的…...

【MySQL】09.索引

索引是用来提高数据库的性能的&#xff0c;但查询速度的提高是以插入、更新、删除的速度为代价的&#xff0c;这些写操作&#xff0c;增加了大量的IO。所以它的价值在于提高一个海量数据的检索速度。 1. 认识磁盘 MySQL 给用户提供存储服务&#xff0c;而存储的都是数据&…...

【备忘】 windows 11安装 AdGuardHome,实现开机自启,使用 DoH

windows 11安装 AdGuardHome&#xff0c;实现开机自启&#xff0c;使用 DoH 下载 AdGuardHome解压 AdGuardHome启动 AdGuard Home设置 AdGuardHome设置开机自启安装 NSSM设置开机自启重启电脑后我们可以访问 **http://127.0.0.1/** 设置使用 AdGuardHome DNS 效果图 下载 AdGua…...

[Windows] 游戏常用运行库- Game Runtime Libraries Package(6.2.25.0409)

游戏常用运行库 合集 整合了许多游戏会用到的运行库&#xff0c;支持 Windows XP – Windows 11 系统&#xff0c;并且支持自动检测系统勾选推荐的运行库&#xff0c;方便快捷。 本版特点&#xff1a; By&#xff1a;mefcl 整合常见最新游戏所需运行库 根据系统自动勾选推荐…...

MYSQL order 、group 与row_number详解

一、order by order by A ASC, B DESC,C ASC … 上述语句会先按照A排序&#xff0c;当A相同的时候再按照B排序&#xff0c;当B相同的再按照C排序&#xff0c;并会不按照ABC组合一起排序 二、group by group by A,B,C… select 中的字段必须是group by中的字段&#xff0c;…...

QT之巧用对象充当信号接收者

备注&#xff1a;以下仅为演示不代表合理性&#xff0c;适合简单任务&#xff0c;逻辑简单、临时使用&#xff0c;可保持代码简洁&#xff0c;对于复杂的任务应创建一个专门的类来管理信号和线程池任务. FileScanner类继承QObject和QRunnable&#xff0c;扫描指定目录下的文件获…...

《红警2000》游戏信息

游戏背景&#xff1a;与《红色警戒》系列的其他版本类似&#xff0c;基于红警 95 的背景设定&#xff0c;讲述了第二次世界大战期间&#xff0c;世界各国为了争夺全球霸权而展开战争。游戏画面与音效&#xff1a;在画面上相比早期的红警版本有一定提升&#xff0c;解析度更高&a…...

Vue3 + ThinkPHP8 + PHP8.x 生态与 Swoole 增强方案对比分析

一、基础方案&#xff1a;Vue3 ThinkPHP8 PHP8.x 传统架构 优点 ​成熟稳定​ 组合经过长期验证&#xff0c;文档和社区资源丰富ThinkPHP8 对PHP8.x有良好支持&#xff0c;性能比PHP7提升20-30% ​开发效率高​ TP8的ORM和路由系统大幅减少样板代码Vue3组合式API Vite开发…...

(九)PMSM驱动控制学习---高阶滑膜观测器

在之前的文章中&#xff0c;我们介绍了永磁同步电机无感控制中的滑模观测器&#xff0c;但是同时我们也认识到了他的缺点&#xff1a;因符号函数带来的高频切换分量&#xff0c;使用低通滤波器引发相位延迟&#xff1b;在本篇文章&#xff0c;我们将会介绍高阶滑模观测器的无感…...

25年上半年五月之软考之设计模式

目录 一、单例模式 二、工厂模式 三、 抽象工厂模式 四、适配器模式 五、策略模式 六、装饰器模式 ​编辑 考点&#xff1a;会挖空super(coffeOpertion); 七、代理模式 为什么必须要使用代理对象&#xff1f; 和装饰器模式的区别 八、备忘录模式 一、单例模式 这个…...

Mongo DB | 多种修改数据库名称的方式

目录 方法一&#xff1a;使用 mongodump 和 mongorestore 命令 方法二&#xff1a;使用 db.copyDatabase() 方法 方法三&#xff1a;使用 MongoDB Compass 在 MongoDB 中&#xff0c;更改数据库名称并不是一个直接的操作&#xff0c;因为 MongoDB 不提供直接重命名数据库的命…...

QListWidget的函数,信号介绍

前言 Qt版本:6.8.0 该类用于列表模型/视图 QListWidgetItem函数介绍 作用 QListWidget是Qt框架中用于管理可交互列表项的核心组件&#xff0c;主要作用包括&#xff1a; 列表项管理 支持动态添加/删除项&#xff1a;addItem(), takeItem()批量操作&#xff1a;addItems()…...

Python类属性与实例属性的覆盖机制:从Vector2d案例看灵活设计

类属性与实例属性的交互机制 Python中类属性与实例属性的关系体现了语言的动态特性。当访问一个实例属性时&#xff0c;Python会首先查找实例自身的__dict__&#xff0c;如果找不到&#xff0c;才会去查找类的__dict__。这种机制使得类属性可以优雅地作为实例属性的默认值。 …...

QML与C++交互2

在QML与C的交互中&#xff0c;主要有两种方式&#xff1a;在C中调用QML的方法和在QML中调用C的方法。以下是具体的实现方法。 在C中调用QML的方法 首先&#xff0c;我们需要在QML文件中定义一个函数&#xff0c;然后在C代码中调用它。 示例 //QML main.qml文件 import QtQu…...

EtherNet/IP机柜内解决方案在医疗控制中心智能化的应用潜能和方向分析

引言 在数智化转型浪潮席卷各行各业的今天,医疗领域同样面临着提升运营效率、改善患者体验和加强系统可靠性的多重挑战。Rockwell Automation于2025年5月20日推出的EtherNet/IP机柜内解决方案,为医疗中心的自动化升级提供了一种创新路径。本报告将深入分析这一解决方案的核心…...

springboot中各模块间实现bean之间互相调用(service以及自定义的bean)

springboot中各模块间实现bean之间互相调用&#xff08;service以及自定义的bean&#xff09; https://blog.csdn.net/qq_29477175/article/details/122827446?ops_request_misc&request_id&biz_id102&utm_termspringboot%E5%A4%9A%E6%A8%A1%E5%9D%97%E4%B9%8B%E…...

RabbitMQ 可靠性保障:消息确认与持久化机制(二)

四、持久化机制&#xff1a;数据安全的护盾 &#xff08;一&#xff09;交换机持久化 交换机持久化是确保消息路由稳定的重要保障 。在 RabbitMQ 中&#xff0c;交换机负责接收生产者发送的消息&#xff0c;并根据路由规则将消息路由到相应的队列 。如果交换机在 RabbitMQ 重…...

QML学习07Property

Property 1、Property1.1 定义控件1.2 给控件取别名&#xff0c;不向外暴露控件名字 2、总结 1、Property property int myTopMargin: 0 property int myBottomMargin: 0 property real myReal: 0.0 //双精度浮点数 property string myString: "test" property…...