6.5840 - LAB2

LAB2 任务地址 源码地址 实现思路 任务描述 此 LAB 比较适合使用增量模型一步一步来,所以实现思路也根据任务的不同要求一步一步递进。 当你看到这里时默认你已经看过了任务描述,这里再简单复述一下。我们需要构建一个 KVServer,以及一个 Clerk,Clerk 可以放松两个请求,Get 和 Put,前者试图获取一个key 的 value,后者通过 version 的值来尝试进行 kv 的创建或者更新。 构建一个基本的 KVServer 像前一个 LAB 一样,为了方便起见我们自定义一个结构体来存储 kv 和 version。如下: type Item struct { K,V string Version rpc.Tversion } 那对于我们的 server 来讲,我们自然是需要一个变量来存放item,并且最好能够快速的获取和更新结果,那根据这种需求,我们自然可以想到使用 map,并且在 map 中存放指针而不是值对象,就可以方便快速的获取以及更新对象。此外,我们还需要一个锁,以此来避免可能会有的数据竞争问题,避免一个 rpc 在操作某一个 key,另一个 rpc 在读取这个 key,导致冲突。 所以我们的 Server 可以定义如下: type KVServer struct { kvs map[string]*Item mu sync.Mutex } 接下来只需要处理逻辑上的问题就好了,没有难点。 你直接去看源代码可能会看到有sync.Map,这是我在假设读多写少的场景下做的优化,相信你可以理解其中的逻辑。 使用键值存储实现锁 对于这个需求一开始我不太理解,看了好一会儿才看懂。大致的意思就是很多个 clerk 共享同一个 key,我们要做的就是基于这个 key 实现一个 clerk(lock)对这个 kv 对的掌控权限,相当于所有权。 那大致的思路就很简单了,我们可以给每一个 lock 一个独一无二的 secret,让他给这个共享的 key 赋值为自己的这个密钥,那对于其他的 lock 而言,通过 get 请求获取到的 value 不是自己的密钥也不为空,那就说明这个 key 此时正在被别的 lock 拥有,那就不断循环尝试判断这个 value 就行了,等到 value 为空的那一刻,也就说明这个 key 被别人释放了,那就可以赋值为自己的 secret,让别人也无法获取。 ...

February 21, 2025

6.5840 - LAB1

LAB1 任务地址 源码地址 实现思路 Lab1主要有两个角色:coordinator 与 worker,worker 主动的向 coordinator 请求任务。总共两个任务: map 与 reduce,并且 reduce 依赖于 map 的结果。所以我们可以自然的让 coordinator 将整个流程分为两个模块来进行处理:一个是处理 map 任务,一个是处理 reduce 任务。 在有了大致的思路之后,下一步显然是具体的去处理任务。那处理任务又分为两个步骤,分配任务与校验任务是否完成。在这个lab 中,分配任务与校验任务都是被动的,只能依靠 worker 的请求来进行处理。所以对于 map 阶段来说,我们显然可以设计两个 RPC 请求,一个是请求分配 map 任务,一个是告知任务已完成。 有了分配任务和校验的思路之后,下一步就是如何分配与校验。根据任务要求,coordinator 主要校验的是任务是否超时,那显然我们在分配任务的时候要有一个 record 来记录某个任务被分配出去的时间,然后等到 worker 主动报备任务完成时,coordinator 来校验这个时候任务是否已经超时。但显然还有另一种情况存在,那就是 worker 接收到任务之后宕机或者因为某些原因拖延完成时间,没有办法给 coordinator 及时的反馈,那 coordinator 就需要一个主动的机制去校验一个任务何时失败,让一个任务没有反馈或者超时的时候,coordinator 可以自行的控制这个 task 接下来的行为。需要注意一点:既然我们要具体的判断某一个任务,那我们就需要对这个任务进行唯一标识,这样才能更好的存储与判断任务的状态。 说完了 coordinator 的要做的事情,worker 的工作流程就相对简单,不管是正常完成,超时完成,接收到了但是不完成。coordinator 都已经有对应的机制了,那需要做的就是在请求到任务之后去做然后给 coordinator 反馈就可以了。唯一需要注意的就是,worker 是需要转变的,比如现在请求 map 任务,过一会儿请求 reduce 任务,最后如果没有任务可做的话,还需要能够退出进程。那这里我们再结合前面的不同阶段引入一个 period(时期)的概念,为了避免混乱,我们规定时期的轮转由 coordinator 来控制,那我们可以简单的让每一次 rpc请求的 response 都带上 coordinator 的period,让 worker 改变自己的 period,当 period 是退出(全部任务完成)的时候,worker 也跟着退出就好了。 ...

February 20, 2025

在Go语言中如何更详细地处理与包装错误

1.Go语言中的错误处理基础 在go语言中,我们使用error接口来处理错误,error接口的定义十分简单,只需要包含有 Error() string 方法即可。但如何从一层一层的程序中详细完整的传递,记录错误,以及如何去方便快捷的找到错误根源,显然是一个难点。本文就针对这个难点来发表一些浅薄的个人见解。(注:本文更注重于表述个人见解,如若对您有帮助,更建议结合个人项目搭建错误处理与日志体系) type error interface { Error() string } 我认为处理错误的最重要的两点在于错误链与调用栈,当一个error 发生时,我们需要在调用处知道完整的错误链,以及在错误发生处知道完整的调用栈,有了这两点我们就既可以得知环环相扣的错误信息,又能根据调用栈直截了当的知晓调用流程和错误源头。本篇文章不会对两者的结合展开叙述,只是单独的解释和演示相关机制。 2.Go1.13的错误包装机制 从Go 1.13开始,标准库提供了错误包装功能。你可以使用fmt.Errorf和%w格式化动词来包装错误,这样可以保留原始错误的上下文信息,同时添加更多描述。 if err := do(); err != nil { return fmt.Errorf("failed to do..., %w",err) } 这里给出应用错误链的一个简单demo。 func main() { err := do() e := errors.Unwrap(err) fmt.Printf("err: %v\n", err) fmt.Printf("origin: %v\n", e) } func do() error { if err := learn(); err != nil { return fmt.Errorf("do something failed, %w", err) } return nil } func learn() error { return fmt.Errorf("can`t learn") } 最终的输出如下: err: do something failed, can`t learn origin: can`t learn 看到这里相信你已经有了一些奇思妙想了,利用错误链的机制不仅可以传递错误信息,还因为Unwrap的存在而有了更多可能性,现在我来介绍如何自定义错误类型并传递额外信息。 3.自定义错误类型 首先,我们定义一个自己的结构体,和一个满足自己结构体方法的接口。 type Merror interface { Error() string Extra() any } type merror struct { msg string // 存储错误信息 extra int64 // 存储额外的信息(any type) } // New 创建一个新的结构体 func New(msg string, extra ...int64) error { e := &merror{ msg: msg, } if extra != nil { e.extra = extra[0] } return e } func (e *merror) Error() string { return e.msg } func (e *merror) Extra() any { return e.extra } // Format 用于进行格式化的输出,在遇到占位符时转换为我们想要的内容 func (e *merror) Format(state fmt.State, verb rune) { switch verb { case 's', 'v': io.WriteString(state, fmt.Sprintf("msg: %s,extra: %d", e.msg, e.extra)) } } 我们自定义的merror 实现了Error() string 与 Formatter(state,rune) 接口。对于前者,这意味着merror 可以被当成error 传递。对于后者,这让我们可以自己实现我们所想要的的输出格式。 ...

November 25, 2024

tiktok 中的整洁架构

或许整洁架构对你而言有些陌生,让你在review代码的时候感到困惑,没关系,我会向你举例帮助你理解他。 这里就以GitHub - mutezebra/tiktok中user的Register方法为例。 架构: 我们首先来看一下架构图,在此我们主要关注三个层次,从上到下依次为领域层(domain),用例层(usecase)和接口层(interface). 接下来我将举例向你解释他们分别的职能,但在此之前我先说一下我对这三个层次的个人见解: Domain 着重于提供顶层方法,以便可以被整个架构所引用,同时他负责业务核心代码的实现。 Usecase 侧重梳理业务逻辑,主要通过引用Domain层暴露出来的接口或方法来实现业务逻辑,并将数据转换成接口层可理解形式返回。 Interface 着重于处理更底层传来的数据,并将其转换成Usecase或Domain更容易理解和操纵的形式。 在Tiktok中,他大致长这个样子。 Register 在整洁架构中,层级分离会屏蔽较底层的代码,高层并不知道底层实现了什么,但是高层又需要底层的方法,所以我们通常会在高层中定义好接口,然后由底层去实现它,最后再在一个地方将其注入进去,这样高层就可以在不知道底层的具体实现的情况下实现调用底层的方法。 领域层 Domain 1. 定义数据库接口 领域层作为业务的核心处理部分,自然是要统筹全局,提前定义和准备好他所需要的方法。那注册一个用户需要什么呢?首先想到的肯定是把用户信息存储到数据库中啦,但是作于高层来说,自然不需要去做那些脏活累活,我们只需要定义一下这个接口就好了,等下让接口层去做具体的实现。 app/user/domain/repository/db.go // UserRepository defines the operational // criteria for the user repository type UserRepository interface { CreateUser(ctx context.Context, user *User) error // create a new user } 2. 定义用户信息结构 好啦,现在已经可以把用户的信息存进数据库了,那用户信息的具体内容呢?好像还没有,那作为高贵的领域层,我们来定义一下用户信息吧。 为了方便管理,我们在pkg中创建一个名为types的package来统一放置一些由于业务需求而定义的结构体。(pkg的方法被全局使用,也可以理解为Domain层) pkg/types/model.go // User is the standards for repo operand objects type User struct { ID int64 `db:"id"` UserName string `db:"user_name"` Email string `db:"email"` PasswordDigest string `db:"password_digest"` Gender int8 `db:"gender"` Avatar string `db:"avatar"` Fans int32 `db:"fans"` Follows int32 `db:"follows"` TotpEnable bool `db:"totp_enable"` TotpSecret string `db:"totp_secret"` CreateAt int64 `db:"create_at"` UpdateAt int64 `db:"update_at"` DeleteAt int64 `db:"delete_at"` } 3. 定义Service结构体 好了,现在用户的信息也有了,那我们看一下这个信息,有username,email,password等等,那自然又引申出一个问题,用户的名字合法吗?email格式是标准的吗?password符合我们的要求吗?存入数据库的话需不需要加密? ...

July 7, 2024