尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

在保证线性一致性的情况下如何读Kv

在保证线性一致性的情况下如何读Kv
📅 发布时间:2026/7/31 9:45:00

先给结论:

这个项目为了保证线性一致性,没有收到Get就直接查本地 KV,而是先把Get也作为一条命令提交到 Raft。等这条Get日志被提交、应用后,才读取本地 KV 并返回结果。

这种方案简单可靠,但每次读都要经过一次 Raft 共识,性能较低。

一、为什么不能直接读本地KV

假设集群有三个节点:

S1:旧Leader S2:新Leader S3:Follower

发生网络分区后,S1可能还以为自己是 Leader,但S2、S3已经选举出新 Leader。

新 Leader 完成写入:

Put("x", "200")

此时:

S2:x = 200 S3:x = 200 S1:x = 100

如果客户端向旧 LeaderS1发起Get("x"),直接读取本地数据会得到100。

但写入200已经成功返回,后续读取却读到了旧值,这就违反线性一致性。

所以:

“节点认为自己是 Leader”不等于“这个节点现在仍然拥有多数派支持”。

二、本项目的线性一致读方案

本项目采用“读请求也进入 Raft 日志”的方式:

客户端发送Get ↓ Leader把Get作为Op写入Raft ↓ Get日志复制到多数节点 ↓ Get日志被提交 ↓ Apply线程按顺序应用到这个位置 ↓ 通知Get RPC线程 ↓ RPC线程读取本地KV ↓ 返回查询结果

Get请求被包装成:

Op op; op.Operation = "Get"; op.Key = args->key(); op.ClientId = args->clientid(); op.RequestId = args->requestid();

然后调用:

m_raftNode->Start(op, &raftIndex, &term, &isLeader);

如果当前节点不是 Leader:

if (!isLeader) { reply->set_err(ErrWrongLeader); return; }

客户端就会换节点重试。

三、为什么把Get写入Raft就能避免旧读

假设日志顺序是:

index=8:Put("x", "100") index=9:Append("x", "A") index=10:Get("x")

状态机必须按照日志顺序应用:

先执行 index=8 再执行 index=9 最后到达 index=10

当Get对应的index=10已经应用时,可以确定:

index <= 10 的已提交写操作都已经应用到了本地KV

这条Get日志相当于一个读屏障 Read Barrier。

因此读取结果至少包含排在它前面的所有已提交写操作。

例如:

Put("x", "100") 已成功返回 Get("x") 随后开始

Get经过 Raft 后,一定不能越过前面已提交的Put,所以不能读到Put之前的旧值。

四、timeOutPop()在等什么

调用Start()后,只代表 Raft Leader接受了日志:

m_raftNode->Start(op, &raftIndex, &term, &isLeader);

不代表该日志已经提交。因此 RPC线程还要等待:

chForRaftIndex->timeOutPop( CONSENSUS_TIMEOUT, &raftCommitOp );

它等待 Apply线程通知:

这个日志位置上的命令已经提交并应用了。

流程是:

Get RPC线程 Raft Apply线程 | | | Start(Get) | |----------------------------->| | | 复制并提交 | timeOutPop()阻塞等待 | | | 收到ApplyMsg |<------ raftCommitOp ----------| | 读取KV并返回 |

五、没有超时时怎么处理

图片下半部分是:

if (raftCommitOp.ClientId == op.ClientId && raftCommitOp.RequestId == op.RequestId) { std::string value; bool exist = false; ExecuteGetOpOnKVDB(op, &value, &exist); if (exist) { reply->set_err(OK); reply->set_value(value); } else { reply->set_err(ErrNoKey); reply->set_value(""); } } else { reply->set_err(ErrWrongLeader); }

必须检查:

ClientId是否相同 RequestId是否相同

原因是 Raft 领导者可能发生变化。

旧 Leader可能认为当前请求位于:

index = 10

但它还没有提交就失去领导权。新 Leader可能用其他命令覆盖index=10。

RPC线程虽然等到了index=10的 Apply消息,但不一定是自己的请求,因此不能只检查日志下标。

必须确认:

raftCommitOp.ClientId == op.ClientId raftCommitOp.RequestId == op.RequestId

如果不一致,就让客户端重试。

六、KV究竟在哪里读取

真正读取跳表的代码是:

void KvServer::ExecuteGetOpOnKVDB( Op op, std::string* value, bool* exist ) { m_mtx.lock(); *value = ""; *exist = false; if (m_skipList.search_element(op.Key, *value)) { *exist = true; } m_lastRequestId[op.ClientId] = op.RequestId; m_mtx.unlock(); }

互斥锁保证读取时不会和另一个写操作交叉修改。

在这个实现中,可以把两个时间点区分开:

Get日志应用:建立读屏障,保证之前的写已经应用。

锁内读取 KV:真正确定返回值,可看作实际线性化点。

如果在读屏障后,又有一个并发写先应用,Get读到更新后的值也是合法的,因为读和这个写的执行时间发生了重叠。

七、超时分支

代码是:

if (!chForRaftIndex->timeOutPop(...)) { bool isLeader = false; m_raftNode->GetState(&term, &isLeader); if (ifRequestDuplicate(op.ClientId, op.RequestId) && isLeader) { ExecuteGetOpOnKVDB(op, &value, &exist); // 返回查询结果 } else { reply->set_err(ErrWrongLeader); } }

timeOutPop()返回false表示等待超时。

但超时不代表 Get 一定没有执行,可能是:

Get已经提交 Get已经执行 Apply通知到达较晚 RPC线程先发生超时

所以代码检查去重表:

ifRequestDuplicate(op.ClientId, op.RequestId)

如果去重表中已经记录了这个请求,说明该请求以前执行过,可以再次读取。

否则没有证据证明这条 Get 已经通过 Raft建立读屏障,只能返回:

ErrWrongLeader

客户端使用相同的ClientId + RequestId换节点重试。

八、isLeader检查需要特别注意

代码通过:

m_raftNode->GetState(&term, &isLeader);

检查自己是否仍然是 Leader。

但严格来说:

只检查本地isLeader,不能单独证明当前节点仍然得到多数派支持。

一个网络隔离的旧 Leader在收到更高任期消息之前,仍可能认为自己是 Leader。

因此,不能写成:

if (isLeader) { 直接读取本地KV; // 对新Get不安全 }

图片中的代码还要求:

ifRequestDuplicate(...) && isLeader

即只允许已经执行过的 Get 重试读取。不过更清晰、稳妥的实现是:

等待超时后直接返回可重试错误

或者重新执行一次完整的 Raft读屏障,而不是只依赖本地isLeader。

九、 Get需要去重吗

Get不会修改业务 KV,因此重复执行不会像Append那样产生重复写入。

但是重复执行可能返回不同结果:

第一次Get:x = 100 中间执行Put:x = 200 重试Get:x = 200

在单个请求从调用到最终响应的整个时间区间内,这通常仍可以找到合法的线性化点。

但如果要求重复请求必须返回完全相同的结果,去重表就不能只保存:

ClientId -> LastRequestId

还要缓存原始响应:

struct ClientRecord { int lastRequestId; std::string lastValue; Err lastError; };

重复请求直接返回第一次查询结果,不重新读取。

十、生产系统常见的三种方案

方案一:Get写入Raft日志

也就是本项目的方案:

Get -> Raft日志 -> 多数派提交 -> 应用 -> 本地读

优点:

实现简单 容易证明线性一致性 读写具有统一顺序

缺点:

每次读都要复制日志 延迟高 Raft日志增长快

方案二:ReadIndex

生产系统更常用:

Leader向多数节点确认自己仍然是Leader 获取安全的commitIndex作为readIndex 等待lastApplied >= readIndex 读取本地KV

它不需要把每个Get写入日志,但仍然确认了当前 Leader的有效性。

方案三:Leader Lease

Leader在租约有效期内直接读取本地数据,性能最高,但依赖时钟和租约条件,实现与正确性证明更加复杂。

相关新闻

  • Simulink建模效率革命:Matlab脚本自动化实战指南
  • 抓包鹰工具连接表功能,实时查看本机所有活动连接与进程归属
  • 严查各类灰色收费!2026 上海黄金回收收费标准公示,多项杂费免除 - 日常比对手册

最新新闻

  • 容易被忽视的男士丝袜细节,选对穿着舒适度翻倍 - 品牌测评网
  • C++辗转相除法求最大公约数:从原理到实战应用详解
  • CTF实战:利用.phtml绕过文件上传黑名单漏洞
  • CTF网络流量分析神器:3分钟掌握CTF-NetA终极指南
  • 老旧Android电视焕新秘籍:mytv-android让你的电视盒子流畅如新
  • 济南新房除甲醛避坑指南,全方位对比之后,倍清环保成为多数家庭首选 - 专注室内空气检测治理

日新闻

  • 7步掌握KMS智能激活工具:Windows和Office永久激活完整方案
  • 如何在Windows上运行iOS应用:ipasim跨平台模拟器终极指南
  • 2026年重庆工伤赔偿律师口碑推荐:洪家木律师用专业赢得信赖 - 本地品牌推荐

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号