22|阶段实操(2):构建一个简单的KV server-基本流程

155 阅读3分钟

正式开始

实现并验证协议层

  1. 先创建一个项目:cargo new kv --lib
  2. 在Cargo.toml中添加依赖
  3. 在根目录下定义abi.proto
  4. 在根目录下创建build.rs,对构建过程进行自定义
  5. 在根目录下创建 examples,写一些代码测试客户端和服务器之间的协议

实现并验证 Storage trait

  1. 支持并发的 HashMap,可以使用 dashmap 创建一个 MemTable 结构,来实现 Storage trait
  2. 现在 src/storage/memory.rs 还没有被添加,所以 cargo 并不会编译它。要在 src/storage/mod.rs 开头添加代码

实现并验证 CommandService trait

最后的拼图:Service 结构的实现


/// Service 数据结构
pub struct Service<Store = MemTable> {
    inner: Arc<ServiceInner<Store>>,
}

impl<Store> Clone for Service<Store> {
    fn clone(&self) -> Self {
        Self {
      inner: Arc::clone(&self.inner),
        }
    }
}

/// Service 内部数据结构
pub struct ServiceInner<Store> {
    store: Store,
}

impl<Store: Storage> Service<Store> {
    pub fn new(store: Store) -> Self {
        Self {
            inner: Arc::new(ServiceInner { store }),
        }
    }

    pub fn execute(&self, cmd: CommandRequest) -> CommandResponse {
        debug!("Got request: {:?}", cmd);
        // TODO: 发送 on_received 事件
        let res = dispatch(cmd, &self.inner.store);
        debug!("Executed response: {:?}", res);
        // TODO: 发送 on_executed 事件

        res
    }
}

// 从 Request 中得到 Response,目前处理 HGET/HGETALL/HSET
pub fn dispatch(cmd: CommandRequest, store: &impl Storage) -> CommandResponse {
    match cmd.request_data {
        Some(RequestData::Hget(param)) => param.execute(store),
        Some(RequestData::Hgetall(param)) => param.execute(store),
        Some(RequestData::Hset(param)) => param.execute(store),
        None => KvError::InvalidCommand("Request has no data".into()).into(),
        _ => KvError::Internal("Not implemented".into()).into(),
    }
}
  1. Service 结构内部有一个 ServiceInner 存放实际的数据结构,Service 只是用 Arc 包裹了 ServiceInner。这也是 Rust 的一个惯例,把需要在多线程下 clone 的主体和其内部结构分开,这样代码逻辑更加清晰
  2. execute() 方法目前就是调用了 dispatch,但它未来潜在可以做一些事件分发。这样处理体现了 SRP(Single Responsibility Principle)原则
  3. dispatch 其实就是把测试代码的 dispatch 逻辑移动过来改动了一下

重点

  1. 在 Rust 下,但凡出现两个数据结构 v1 到 v2 的转换,你都可以先以 v1.into() 来表示这个逻辑,继续往下写代码,之后再去补 From 的实现
  2. 如果 v1 和 v2 都不是你定义的数据结构,那么你需要把其中之一用 struct 包装一下,来绕过之前孤儿规则。
  3. 要特别注意:测试代码要围绕着系统稳定的部分,也就是接口,来测试,而尽可能少地测试实现
  4. Dashmap 内部封装了一个优化的 Arc<RwLock<HashMap>>

小结

一个有潜在生产环境质量的 Rust 项目应该如何开发

  1. 要对需求有一个清晰的把握,找出其中不稳定的部分(variant)和比较稳定的部分(invariant)

    在 KV server 中,不稳定的部分是,对各种新的命令的支持,以及对不同的 storage 的支持。所以需要构建接口来消弭不稳定的因素,让不稳定的部分可以用一种稳定的方式来管理

  2. 代码和测试可以围绕着接口螺旋前进,使用 TDD 可以帮助我们进行这种螺旋式的迭代。

    在一个设计良好的系统中:接口是稳定的,测试接口的代码是稳定的,实现可以是不稳定的

好用链接

build.rs prost_build thiserror定义错误现在 src/storage/memory.rs 还没有被添加,所以 cargo 并不会编译它。要在 src/storage/mod.rs 开头添加代码

精选问答

  1. dev-dependencies 与 dependencies的第三方crate是如何划分的?

    a. examples / test 里用的库是 dev-dependencies + dependencies

    b. build.rs 里用到的库是 build-dependencies + dependencies

    c. 正常代码(库/二进制)用的是 dependencies

  2. tcp的半包粘包等,是被prost处理掉了吗?

    a. prost 只是 protobuf serialize / deserialize 的工具,并不负责做这样的事情

    b. 半包粘包的问题,是被 async_prost 处理的:github.com/tyrchen/asy…