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

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

先给结论:

这个项目为了保证线性一致性,没有收到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在租约有效期内直接读取本地数据,性能最高,但依赖时钟和租约条件,实现与正确性证明更加复杂。

http://www.jsqmd.com/news/1299518/

相关文章:

  • 2026年性价比高的太和县别墅门情况曝光,哪种更值得选? - 品牌品鉴馆
  • 2026十大美国移民中介综合口碑榜单,实力测评真实客片解析,备选不交智商税 - 工业推荐榜
  • 2026年8月石家庄劳动仲裁律师推荐 陈瑞俊律师|深耕劳动仲裁纠纷,专业靠谱,用工争议全流程处理 - 十大排行榜推荐
  • WarcraftHelper:魔兽争霸3终极兼容性解决方案,让经典游戏重获新生
  • Simulink建模效率革命:Matlab脚本自动化实战指南
  • 数字控制系统信号重建:ZOH与低通滤波器的频率特性分析与设计
  • 智能家居H-Link协议解析与全屋智能化解决方案
  • 抓包鹰工具连接表功能,实时查看本机所有活动连接与进程归属
  • 盲审前最后七天:我每天都在检查什么
  • 合同章挂失登报合规范本:银行工商核验认可的标准文稿指南 - 叮咚办真方便
  • 深度剖析Untrunc:基于MP4原子结构分析的专业视频修复工具终极指南
  • 绕了一上午,我才搞懂 OpenClaw 为什么不回我消息
  • 上海离婚损害赔偿律所:过错认定标准与赔偿额度实务解析 - 品牌深度评测
  • 银行校招备考时间线规划:进航教育根据备考周期定制合适的备考计划 - 银行求职专家
  • 严查各类灰色收费!2026 上海黄金回收收费标准公示,多项杂费免除 - 日常比对手册
  • 中小微企业级应用-AI超级员工系统,2026真实避坑经验分享 - 米諾
  • 注销公告登报线上办理全攻略:企业简易注销业务指南 - 叮咚办真方便
  • 绝区零一条龙:全自动游戏辅助工具的终极使用指南
  • 360N7手机Root教程:解锁Bootloader、刷入TWRP与Magisk全攻略
  • 微信小程序云环境切换实战:从配置到部署的完整避坑指南
  • 破解嵌入式安全困局|先御PreDefsOS V2.0,中高端设备原生可信安全底座
  • Kotlin算法优化与面试实战指南
  • 医生正在被替代?不——但37.6%的放射科医师已转向AI协作者角色,深度访谈11家智慧医院转型实录
  • 老板IP数字人内容生产指南:从表达定位到多平台发布
  • 轻量级任务管理系统框架开发总结
  • 山东邦达新型建材有限公司:全国雪弗板生产厂家,布局山东临沂等地区,绿色建材优选 - 十大品牌榜
  • 2026民办四大名校软硬件条件实力解析,避坑择校零套路 - myqiye
  • 终极指南:如何用OnmyojiAutoScript一键解放双手,轻松玩转阴阳师
  • Rust实战:从命令行工具到GUI应用,构建文件批处理与音乐播放器
  • 【合肥理工学校】招生办电话是多少?升学逆袭路径说明 - zshll