0.前言
距上一篇分享LZW压缩算法已经过去了几个月了,是时候开新坑了。
最近花了点时间入门了一点Rust,在网上寻找学习资源的时候,发现了这个Tokio官方出的mini-redis,这是一个使用Rust实现的Redis服务端和客户端。但只实现了核心的GET、SET、PUBLISH、SUBSCRIBE功能。简单研究了一下,确实比较适合对初期学习到的各种Rust理念和操作进行一个巩固。
本期先分享mini-redis的数据结构部分。
1.mini-redis
mini-redis数据结构
一切从简,按照最简单最基本的方式来实现Redis。先来想一下,怎么存Redis中的键值对?最简单的方式就是用HashMap,所以先定义一个Redis数据状态类型:
struct State {
entries: HashMap<String, Bytes>,
}
注意到Redis中的key是有过期时间的,所以我们需要修改下entries的值的类型,增加一个过期时间字段:
struct Entry {
data: Bytes,
expires_at: Option<Instant>,
}
struct State {
entries: HashMap<String, Entry>,
}
这样看起来,存储键值对的部分就比较完善了。
接下来解决过期时间的问题,希望能有一种数据结构能直接取到最早过期的key,同时要考虑到多个key在同一时刻过期的问题,所以我们需要用BTreeSet加元组来存储过期时间,如下所示:
struct State {
entries: HashMap<String, Entry>,
expirations: BTreeSet<(Instant, String)>,
}
为什么是BTreeSet,首先BTree部分没啥争议,因为我们需要其中的元素有序,这样才能最快找到最早的过期时间。而Set是为了确保同一个过期时间+key的组合只有一个,想一下,存多个相同的过期时间+key组合也没什么意义。
这样一来,Redis数据存储部分的数据结构就设计完成了。
Redis中还有一项比较重要的功能:发布-订阅,我们来实现它。还得用HashMap,key是订阅的数据键,value则是一个消息发送者,当有一个key需要发送广播消息给订阅该key的订阅者时,直接取出发送者调用发送即可。所以我们进一步扩展State的结构:
struct State {
entries: HashMap<String, Entry>,
expirations: BTreeSet<(Instant, String)>,
pub_sub: HashMap<String, broadcast::Sender<Bytes>>,
}
既然是用来存取数据的,我们要保证最起码的数据一致性,同一个数据,同一时刻,不能有多个写者。用最基本的互斥锁来实现就好,读写的时候都需要先获取锁,之后再进行读写,而没有获取到锁的则等待。我们在State结构上包一层,于是有了:
struct Shared {
state: Mutex<State>,
}
了解Rust的应该清楚,Rust中通过所有权系统来保证内存安全,但所有权同时也限制了数据共享。而我们的Redis不可能一次只服务一个客户端,那要如何实现多线程之间的数据共享呢?也很简单,Rust提供了Arc(原子引用计数),这个结构通过Clone可以为共享的数据创建一个引用指针,同时增加引用计数器。所以我们在Shared结构上再包一层Arc,就有了:
struct Db {
shared: Arc<Shared>,
}
数据结构之上的Redis操作
有了基础的数据结构定义后,我们来实现所需要的Redis能力。首先构造函数得有:
impl Db {
pub(crate) fn new() -> Db {
let shared = Arc::new(Shared {
state: Mutex::new(State {
entries: HashMap::new(),
pub_sub: HashMap::new(),
expirations: BTreeSet::new(),
}),
});
Db { shared }
}
}
get操作是一个纯读的操作,所以我们获取state的不可变引用后,获取互斥锁,然后通过HashMap的get方法来获取所需的值:
更多...