Key/value server with reliable network

在一个可靠的网络下,我们不会丢失消息,实现键值存储的客户端和服务器端

三个重点对象:

Server Clerk RPC

只要 Server 运行我们发送的消息一定会被接收,实现的方法通过 RPC 发送请求 Server 端就会做相应的处理

注意的问题:

我对框架进行了修改, 我并不知道实验使用的自制的 labrpc 框架! 与 net/rpc 包有一些差距的地方在于,我按照包的形式,修改了函数的返回值!

root@Zhonghe:~/6.824/6.5840/src/kvsrv1# go test -run Reliable
One client and reliable Put (reliable network)...
2025/10/25 20:59:06 labrpc.Service.dispatch(): unknown method Get in KVServer.Get; expecting one of []
exit status 1
FAIL    6.5840/kvsrv1   0.006s

导致框架无法注册我错误的函数签名! 导致我无法进行通讯,这个问题我想了很久! 于是我学习了 labrpc 学习 go 的反射机制。

对于 getput 操作我们需要注意什么?

版本号控制 锁的互斥性访问 版本号控制

思考:

锁的作用是什么? 我们为什么要实现锁? 版本控制的目的是什么?

加锁的核心职责:

确保服务器内部的并发安全,防止多个线程(goroutines)同时访问和修改共享数据结构

本地化,局限在单个服务器实例的内存中。加锁解决的是 线程级并发 问题,与分布式系统的网络重试或 Raft 复制无关。

版本控制的核心职责:

幂等性

一个操作被称为幂等的,如果多次执行它产生的结果与单次执行相同,且不会引入额外的副作用。在数学或编程上下文中,这类似于函数 $ f(x) $ 满足 $ f(f(x)) = f(x) $。

如果没有版本控制,重复的 Put 操作可能多次更新键值,导致数据错误(如意外覆盖值)。

我对此的困惑是(如果我们不加版本控制 put操作也不会被重复的请求影响数据的正确性) 群里的以为老哥(根据业务场景,涉及到修改操作时我们必须考虑 幂等性 )一下示例可能会解决大家对此的困惑:

示例:

客户端 A 发送 Put("x", "a")。服务器执行,设置 "x" = "a",但响应因网络故障丢失。 客户端 B 发送 Put("x", "b"),服务器执行,更新 "x" = "b"(因为无版本控制,直接覆盖)。 客户端 A 重试 Put("x", "a")(相同请求),服务器再次执行,覆盖 "x" = "a"。

加以控制:

第 1 步:客户端 A 执行 Get(“x”),观察到版本 0 没有值(或为空)。 第 2 步:客户端 A 发送 Put(“x”, “a”, version=0)。服务器检查:键不存在且 version==0,因此它创建了 version=1 的 “x” = “a”。由于网络故障而失去响应。 第 3 步:客户端 B 执行 Get(“x”),观察到 “x” = “a” 且 version=1。 第 4 步:客户端 B 发送 Put(“x”, “b”, version=1)。服务器检查:当前版本=1 与提供的版本=1 匹配,因此它将“x” = “b”更新为 version=2。 第 5 步:客户端 A 不知道失败,重试 Put(“x”, “a”, version=0)。服务器检查:当前版本=2 与提供的版本=0 不匹配,因此它使用 ErrVersion 拒绝。不发生更新;“x”仍为“b”,版本=2。

客户端 B 的更新将被保留

Implementing a lock using key/value clerk

构建分布式锁,我对分布式锁起初没有什么理解,但是核心思想是确保用户的操作协调对共享资源的访问。 我们在客户端通过每次操作对 Clerk 进行操作

问题:

我现在的困惑是 在分布式环境中 我们仅启动了一个kvServer 我们使用多个客户端访问这个KVServer 我们使用互斥锁可以达到共享资源的访问吧? 为什么还要分布式锁?

理解丢失更新 #75

示例:

读 改 写 三个流程:

如果只有一个客户端进行这三个操作,那没有任何问题,如果多个客户端同时进行,即使我们有本地锁(也就是互斥锁)的加持,我们无法保证发事务的完整性 !

多个 Get 几乎是同时操作,我们加的本地锁,只能保证同一时间内,保证数据结构的安全,但是他不会让我们的逻辑变得更加合理

多个客户端同时读到相同的值,最终多个put操作进行,看似我们有本地锁 + 版本控制, 实则 Put 操作,会使第二个操作的 Put 丢失更新 , 第二个操作以及其他操作必须重试, 按照正常的逻辑, 有几个操作 我们的Put就会相应的做出变化, 实则其他的操作出现了丢失更新

mutex不控制 客户端的行为整个事务(读-改-写) ,

version检测到B的冲突,但只是“拒绝”B,没帮B“ 排队重试”到正确状态

我个人的认为这就是我们需要分布式锁的原因!

分布式锁的作用:锁强制“读-改-写”序列互斥执行:

A获取锁("balance_lock"),完成Get(100, 5) -> 计算50 -> Put(50, 5),释放锁。 B尝试获取锁,失败(A持有),循环重试直到A释放,B获取锁后,Get(50, 6) -> 计算0 -> Put(0, 6),释放。A和B的更新都生效,没丢失。

Key/value server with dropped messages

ErrMaybe:

因为网络的不可靠性, 客户端(Clerk)必须 自己重试 RPC 请求,否则请求可能永远无效。

如果是回复丢了,而不是请求丢了,那么重试会让服务器执行两次。

rpc.ErrMaybe 结果不确定,可能已经写入,也可能没有,在不增加服务器复杂性的前提下,只靠客户端实现“尽力而为的可靠性”。

通过不断尝试

Implementing a lock using key/value clerk and unreliable network

在不可靠网络 + 不确定状态下,如何安全地使用分布式存储接口(Put/Get)

死锁: 锁被卡在某个状态(已上锁),所有客户端都认为自己没拿到锁,也不能释放它。 Put 成功了,但客户端没收到回复(说明了所以经被正常获取到了 但是回复的包丢失了 我们以为超时了!)所以锁被永久卡住了。

客户端 A 拿到锁后挂了,锁状态没释放, 其他客户端永远拿不到锁,系统再次陷入死锁。