基于Raft分布式Kv存储:Clerk
一、Clerk保存了哪些信息Clerk主要有四个成员std::vectorstd::shared_ptrraftServerRpcUtil m_servers; std::string m_clientId; int m_requestId; int m_recentLeaderId;1.m_serversstd::vectorstd::shared_ptrraftServerRpcUtil m_servers;保存所有 KVServer 的 RPC 客户端对象。例如集群有三个节点node0: 127.0.0.1:8000 node1: 127.0.0.1:8001 node2: 127.0.0.1:8002那么m_servers[0] - 访问 node0 的 RPC 对象 m_servers[1] - 访问 node1 的 RPC 对象 m_servers[2] - 访问 node2 的 RPC 对象注意这些对象不代表对应节点一定是 Leader它们只是“访问服务器的客户端代理”。2.m_clientId每个Clerk创建时都会生成一个客户端 IDClerk::Clerk() : m_clientId(Uuid()), m_requestId(0), m_recentLeaderId(0) {}作用是区分不同客户端。例如客户端 AclientId abc123 客户端 BclientId xyz789服务端可以根据ClientId RequestId判断一个请求是否已经执行过。3.m_requestId每个客户端请求都有一个递增的请求编号m_requestId; auto requestId m_requestId;例如Put(a, 1) - RequestId 1 Append(a, 2) - RequestId 2 Get(a) - RequestId 3这个编号非常重要因为 RPC 可能出现这种情况客户端发送 Append ↓ 服务器已经执行成功 ↓ 响应在网络中丢失 ↓ 客户端以为失败再次发送 Append如果第二次请求使用新的编号服务器可能会再次执行Append造成数据重复。所以一次逻辑请求在重试过程中必须始终使用同一个RequestId。4.m_recentLeaderId记录最近一次成功处理请求的服务器编号。int m_recentLeaderId;例如上一次 node2 成功m_recentLeaderId 2下一次请求就优先访问 node2。它只是一个性能优化并不保证 node2 现在仍然是 Leader。如果 Leader 发生变化node2 会返回ErrWrongLeader然后 Clerk 再尝试其他节点。二、测试客户端main截图上方的main主要是一个测试程序大致流程是int main() { Clerk client; client.Init(test.conf); client.Put(...); client.Append(...); std::string value client.Get(...); }它做了三件事第一步创建 ClerkClerk client;此时会调用构造函数m_clientId Uuid(); m_requestId 0; m_recentLeaderId 0;客户端拥有了自己的身份但还不知道服务器地址。第二步初始化服务器连接client.Init(test.conf);Init()会读取配置文件中的所有节点node0ip node0port node1ip node1port node2ip node2port然后为每个节点创建一个 RPC 客户端代理。第三步调用 KV 操作client.Put(...) client.Append(...) client.Get(...)这些函数看起来像本地函数调用但实际上内部都会经过 RPC 网络通信。例如client.Put(key, value);实际上会走Clerk::Put() ↓ Clerk::PutAppend() ↓ RPC 调用 KvServer::PutAppend()三、Clerk::Init()核心代码是MprpcConfig config; config.LoadConfigFile(configFileName.c_str());这一步加载配置文件。然后循环读取节点地址for (int i 0; i INT_MAX - 1; i) { std::string node node std::to_string(i); std::string nodeIp config.Load(node ip); std::string nodePortStr config.Load(node port); if (nodeIp.empty()) { break; } ipPortVt.emplace_back( nodeIp, atoi(nodePortStr.c_str()) ); }假设配置文件是node0ip127.0.0.1 node0port8000 node1ip127.0.0.1 node1port8001 node2ip127.0.0.1 node2port8002读取之后得到ipPortVt { {127.0.0.1, 8000}, {127.0.0.1, 8001}, {127.0.0.1, 8002} }然后为每个节点创建auto* rpc new raftServerRpcUtil(ip, port); m_servers.push_back( std::shared_ptrraftServerRpcUtil(rpc) );raftServerRpcUtil的构造函数中又创建 protobuf Stubstub new raftKVRpcProctoc::kvServerRpc_Stub( new MprpcChannel(ip, port, false) );这里有三层对象Clerk ↓ raftServerRpcUtil ↓ kvServerRpc_Stub ↓ MprpcChannel其中Clerk负责业务层重试raftServerRpcUtil封装 RPC 调用kvServerRpc_Stubprotobuf 自动生成的客户端代理MprpcChannel真正负责序列化和 TCP 通信。false表示允许延迟连接。创建MprpcChannel时可以先不连接真正调用 RPC 时再连接服务器。四、PutAppend()1.Put()和Append()只是包装函数void Clerk::Put(std::string key, std::string value) { PutAppend(key, value, Put); } void Clerk::Append(std::string key, std::string value) { PutAppend(key, value, Append); }它们最后都会进入同一个函数PutAppend(key, value, op);区别只是Put - 覆盖原值 Append - 在原值后追加这样可以减少重复代码。2. 给一次逻辑请求分配 RequestIdm_requestId; auto requestId m_requestId;这里的requestId是本次逻辑操作的编号。注意它在while循环外面只增加一次。例如第一次发送RequestId 10 第二次重试RequestId 10 第三次重试RequestId 10不能写成while (true) { m_requestId; }否则每次重试都会变成新请求重复检测就失效了。3. 选择第一次访问的服务器auto server m_recentLeaderId;如果之前 node1 成功过那么server 1这次优先访问 node1。如果是第一次运行m_recentLeaderId 0;所以第一次默认访问 node0。4. 构造 protobuf 请求raftKVRpcProctoc::PutAppendArgs args; args.set_key(key); args.set_value(value); args.set_op(op); args.set_clientid(m_clientId); args.set_requestid(requestId);最终请求里面包含key : 要操作的键 value : 要写入或追加的值 op : Put 或 Append clientId : 当前客户端 ID requestId : 当前请求编号对应的 protobuf 定义在message PutAppendArgs { bytes Key 1; bytes Value 2; bytes Op 3; bytes ClientId 4; int32 RequestId 5; }5. 发起 RPC 调用raftKVRpcProctoc::PutAppendReply reply; bool ok m_servers[server]-PutAppend( args, reply );这句代码表面上只是调用一个普通 C 函数但内部调用链是mermaid flowchart TD A[Clerk::Put] -- B[Clerk::PutAppend] B -- C[构造 PutAppendArgs] C -- D[raftServerRpcUtil::PutAppend] D -- E[kvServerRpc_Stub::PutAppend] E -- F[MprpcChannel::CallMethod] F -- G[序列化请求] G -- H[TCP 发送到 KvServer] H -- I[RpcProvider 分发服务和方法] I -- J[KvServer::PutAppend RPC入口] J -- K[KvServer::PutAppend 业务函数] K -- L[Raft::Start] L -- M[Raft复制并提交日志] M -- N[KV状态机执行] N -- O[生成 PutAppendReply] O -- P[TCP返回响应] P -- Q[Clerk反序列化并处理结果] 在客户端封装中实际代码是MprpcController controller; stub-PutAppend(controller, args, reply, nullptr); return !controller.Failed();这里的ok只表示 RPC 通信是否成功不代表业务一定成功。五、服务器端收到请求后做什么服务器端的入口是 protobuf 规定的 RPC 函数void KvServer::PutAppend( google::protobuf::RpcController* controller, const PutAppendArgs* request, PutAppendReply* response, google::protobuf::Closure* done )它会调用真正的业务函数KvServer::PutAppend(request, response); done-Run();1. 先把 RPC 参数转换成 Raft 命令Op op; op.Operation args-op(); op.Key args-key(); op.Value args-value(); op.ClientId args-clientid(); op.RequestId args-requestid();这里的Op是项目内部使用的 Raft 日志命令。也就是说客户端的 RPC 请求不会直接修改本地 KV 数据而是先变成Raft 日志中的一条命令2. 调用Raft::Start()m_raftNode-Start( op, raftIndex, _, isleader );Start()的作用是把这条命令交给 Raft。如果当前节点不是 Leaderif (!isleader) { reply-set_err(ErrWrongLeader); return; }于是响应会返回给 Clerk当前节点不是 Leader然后 Clerk 换下一个服务器继续尝试。3. 如果当前节点是 LeaderLeader 会把命令加入自己的日志并通过 Raft 的AppendEntriesRPC 复制给其他节点Leader 本地追加日志 ↓ 发送 AppendEntries ↓ Follower 接收日志 ↓ 多数节点确认 ↓ 日志提交 ↓ 各节点 ApplyMsg ↓ KV 状态机执行命令所以PutAppend()的最终一致性不是由 Clerk 完成的而是由 Raft 完成的。Clerk 只负责把请求送到某个服务器并在失败时换节点重试。六、waitApplyCh的作用Leader 接收到客户端请求后不能在Raft::Start()返回时立即告诉客户端成功。因为Start() 返回只说明命令已经提交到 Leader 的日志中不一定已经复制到多数节点也不一定已经真正执行。所以服务端会根据raftIndex建立等待通道waitApplyCh[raftIndex]然后等待 Raft 提交并应用这条日志。chForRaftIndex-timeOutPop( CONSENSUS_TIMEOUT, raftCommitOp );Raft 应用线程收到命令后会执行GetCommandFromRaft(message)然后把执行结果通知给对应的waitApplyCh。于是请求处理过程是RPC线程 Start() 创建 waitApplyCh 等待 Raft 应用结果 Raft应用线程 收到 ApplyMsg 执行 Put/Append 通知 waitApplyCh RPC线程 被唤醒 检查是不是自己的请求 设置 reply 返回客户端七、为什么要判断ClientId RequestIdKVServer 中维护了std::unordered_mapstd::string, int m_lastRequestId;含义是每个客户端最近一次已经执行的 RequestId判断函数是return RequestId m_lastRequestId[ClientId];如果发现请求已经执行过就不再重复执行。例如客户端发送ClientId A RequestId 5 Operation Append Key x Value abc假设服务器已经执行成功但是响应丢失Clerk 再次发送相同请求ClientId A RequestId 5 Operation Append Key x Value abc服务端发现m_lastRequestId[A] 5于是判断这是重复请求不再执行第二次。否则结果就可能从x helloabc错误地变成x helloabcabc这就是RequestId对Append操作尤其重要的原因。八、Clerk 如何处理返回结果核心判断是if (!ok || reply.err() ErrWrongLeader) { server (server 1) % m_servers.size(); continue; }这里包含两种失败。情况一ok false表示 RPC 通信失败例如TCP 连接失败发送失败接收失败请求序列化失败响应反序列化失败。此时客户端不知道服务器有没有执行成功因此仍然使用相同的RequestId重试。情况二reply.err() ErrWrongLeader表示网络通信成功服务器也返回了响应但是业务结果是当前节点不是 Leader此时 Clerk 继续访问下一个节点。例如第一次访问 node0ErrWrongLeader 第二次访问 node1ErrWrongLeader 第三次访问 node2OK成功后m_recentLeaderId server; return;下一次请求就优先访问 node2。情况三reply.err() OK表示RPC 通信成功 当前节点正确处理了请求 Raft 已经提交并应用了命令于是PutAppend()返回用户程序认为写入成功。要特别区分ok true只表示 RPC 层通信成功。reply.err() OK才表示 KV 业务层请求成功。九、Get()和PutAppend()的逻辑基本相同它的流程是生成 RequestId ↓ 构造 GetArgs ↓ 优先访问最近 Leader ↓ 调用 RPC ↓ RPC失败或 ErrWrongLeader ↓ 换下一个节点重试 ↓ ErrNoKey 返回空字符串 ↓ OK 返回 value客户端侧std::string value client.Get(name);服务器返回OK - 返回实际 value ErrNoKey - 返回 ErrWrongLeader - Clerk 换节点重试这个项目中的Get也会经过 Raftm_raftNode-Start(op, raftIndex, _, isLeader);这样可以保证读操作也具有线性一致性而不是直接读取某个可能落后的 Follower。

相关新闻

2024最稀缺AI字体资产:仅剩17个未注册Unicode字形区块,抢注后转售溢价达2100%(附实时监测工具)

2024最稀缺AI字体资产:仅剩17个未注册Unicode字形区块,抢注后转售溢价达2100%(附实时监测工具)

更多请点击: https://intelliparadigm.com 第一章:AI字体资产的稀缺性与商业价值本质 AI生成字体并非简单复刻传统字库,而是融合语义理解、笔触建模与风格迁移能力的高维创作产物。其稀缺性源于三重约束:高质量中文字符集&#x…

2026/10/17 19:02:36 阅读更多 →
Claude Code 是怎样启动的?

Claude Code 是怎样启动的?

Claude Code 是一个跑在终端里的 Agent runtime。 它里面有 Query Loop,有 Tool System、Tasks、State、 Memory、Hooks。 这些东西听起来已经够复杂了。 那么问题来了。 Claude Code 里面塞了这么多东西,为什么用户在终端里敲下 claude 之后&#x…

2026/10/16 12:32:45 阅读更多 →
基于SpringBoot的毕业生派遣管理系统

基于SpringBoot的毕业生派遣管理系统

目 录 摘 要 Abstract 第一章 绪论 1.1选题动因 1.2目的和意义 1.3论文结构安排 第二章 开发环境与技术 2.1 MYSQL数据库 2.2 Tomcat 介绍 2.3 vue技术 2.4 SpringBoot框架 第三章 系统分析 3.1可行性分析 3.2系统流程分析 3.3系统性能分析 第…

2026/10/16 12:18:06 阅读更多 →

最新新闻

HTML字符转义与XSS攻击防御:规则、场景与最佳实践

HTML字符转义与XSS攻击防御:规则、场景与最佳实践

1. 先说清楚&#xff1a;字符转义到底在防什么先在开篇把话说透&#xff1a;HTML字符转义的核心使命&#xff0c;就是防止浏览器把用户输入的内容当成可执行的代码。如果你在某个文本框里输入了<script>alert(1)</script>&#xff0c;服务端原样输出到页面上&#…

2026/10/17 19:02:22 阅读更多 →
WebGL鼠标拖拽旋转的球面映射与四元数实现

WebGL鼠标拖拽旋转的球面映射与四元数实现

1. 为什么鼠标拖拽旋转在WebGL中不是“动动手指”那么简单很多人第一次尝试用鼠标控制WebGL场景中的立方体旋转时&#xff0c;会下意识地想&#xff1a;“不就是监听mousedown→mousemove→mouseup&#xff0c;然后把鼠标位移量直接加到旋转角度上吗&#xff1f;”我当年也是这…

2026/10/17 19:02:22 阅读更多 →
微服务三件套高可用部署实战:Docker Compose搭建Nacos、Seata、Sentinel集群

微服务三件套高可用部署实战:Docker Compose搭建Nacos、Seata、Sentinel集群

讲真的&#xff0c;微服务架构里事务和流控这两块&#xff0c;最容易让团队在“测试环境一切正常、一上线就出事”的怪圈里打转。Nacos、Seata、Sentinel这套组合&#xff0c;基本是Java微服务生态里绕不开的三件套。但网上教程大多是单机版演示&#xff0c;真到要部署成高可用…

2026/10/17 19:02:22 阅读更多 →
Spring Boot实现微信小程序登录全链路实战

Spring Boot实现微信小程序登录全链路实战

1. 项目概述&#xff1a;为什么“Spring Boot 微信小程序登录”是当前最稳的落地组合 最近两周&#xff0c;我帮三个不同背景的团队搭过小程序后端登录体系——有某高校实验室做的校园服务类轻应用&#xff0c;有某初创公司推的本地生活预约工具&#xff0c;还有一个是某传统…

2026/10/17 19:02:22 阅读更多 →
artcraft:用参数化工作流打通像素画与矢量插画的数字创作管线

artcraft:用参数化工作流打通像素画与矢量插画的数字创作管线

artcraft这个名字&#xff0c;第一次听到的人多半会以为是个艺术展览或者某个独立工作室的品牌。但如果我说&#xff0c;它是将“像素画、矢量插画、甚至手工雕刻”揉进同一条数字创作流水线时&#xff0c;你应该能感受到&#xff0c;这是一个典型的用技术思维反哺艺术表达的个…

2026/10/17 19:02:22 阅读更多 →
原生Canvas 2D实现情人节烟花代码:单文件零依赖,可定制情侣生日礼物

原生Canvas 2D实现情人节烟花代码:单文件零依赖,可定制情侣生日礼物

1. 从一行标题拆解出的真实需求第一次看到“烟花代码&#xff0c;情人节&#xff0c;情侣生日礼物代码适用”这个标题&#xff0c;我脑子里冒出来的不是“浪漫”&#xff0c;而是一连串很实际的问题&#xff1a;谁在用&#xff1f;在什么设备上跑&#xff1f;对方看到的是网页、…

2026/10/17 19:01:21 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介&#xff1a;《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南&#xff0c;重点围绕V3与R1两款模型的能力差异与适用场景展开&#xff0c;帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件&#xff0c;压缩包大小约311KB&#xff0c;篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介&#xff1a;这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档&#xff0c;共收录两百余条可直接套用的润色指令&#xff0c;涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度&#xff0c;并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介&#xff1a;本资源是一份基于OpenCV的本科课程设计完整报告&#xff0c;面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生&#xff0c;解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →