前言
自己在学习Redis的时候,看过不少文章,也买了不少的书,但是很多知识刚看完,没过几天就忘记了,重复看,重复忘。而且很多东西看了也仅仅只是记住了,但其实并没有真正的理解它。所谓纸上得来终觉浅,绝知此事要躬行,因此决定自己参考Redis的源码,一步一步的去实现一个简易版本的Redis,加深理解。
Redis整个项目还是比较大的,第一阶段我们先将整个核心的框架搭起来,可以执行简单的get、set命令,后续在基于这个框架来实现后面的各种功能。
整个项目参考
Redis 3.0实现,开发语言使用Golang
本文原始连接:juejin.cn/post/695135…
先看一下我们的实现效果:
项目结构
整个Redis项目主要可以分为以下几大块:
数据结构
主要是数据结构的实现,包括:
- sds 动态字符串的实现
- adlist 双端链表的实现
- dict 字典的实现
- zskiplist 跳跃表的实现
- ziplist zipmap 压缩表、压缩字段的实现
- hyperloglog hyperloglogr的实现
数据类型
- object Redis 的对象(类型)系统实现。
- t_string 字符串键的实现。
- t_list 列表键的实现。
- t_hash 散列键的实现。
- t_set 集合键的实现。
- t_zset 有序集合键的实现。
- hyperloglog HyperLogLog 键的实现
数据库
db Redis的数据库实现
notify 数据库通知功能实现
rdb rdb持久化实现
aof aof持久化实现
客户端和服务端相关
ae* Redis的事件处理器实现,redis自己实现了一套基于reactor模式的事件处理器
networking 网络连接库,负责网络相关的操作,比如发送,接收命令,协议解析,创建/销毁客户端
redis 单机Redis服务器实现
集群相关
replication 复制功能的实现
sentinel sentinel的实现
cluster 集群的实现
以上就是Redis的大概功能划分,其中一些较为独立和边缘的功能没有列在里面,比如监控、慢查询、Lua脚本、事务等等。 我们在实现自己的redis的时候,也会按照上面的结构来划分我们的功能。
实现
下面开始吧,实现我们自己的Redis,我们第一阶段的需求很简单:可以通过Redis的客户端redis-cli连接我们的Redis服务器,并可以进行简单的字符串的get和set操作,以及quit退出操作。 很多功能和数据结构我们在这个阶段都会略过或者直接使用Golang中已有的实现作为替代。
下面在实现过程中,如果涉及到一些这次功能没有使用到的逻辑或原理,会暂且略过,我们后面去实现它的时候会详细说明。
数据结构实现
仅仅实现string操作的话,我们只需要实现几个必要的数据结构即可,包括sds,dict以及robj。
sds实现
sds是Redis中自定义的字符串,sds其实就是char*类型指针,但是在指针前面分配了一个表头sdshdr,里面包含两个unsigned int和一个char[]来指向实际的char数组。
typedef char *sds
struct sdshdr {
unsigned int len; //buf中实际使用的大小 使用了 5
unsigned int free; //buf中的空闲大小 剩余 5
char buf[]; //实际的char数组, ['H','E','L','L','O','\0', , , , , ]
};
相关源文件:sds.h,sds.c
由于Golang中已经有完善的string类型了,我们就不实现sds了,直接使用string替代:
sds.go
type sds string
dict实现
dict是Redis中定义的字典结构,在大量的地方被使用到,我们存储的数据实际上也是存储在dict中,dict中主要由3个部分组成:
typedef struct dict //包含两个hash表,一个用于存储数据,另一个用于渐进式rehash
typedef struct dictht //hash表,key通过hash函数后存放到hash表中,表中每个元素都是一个链表
typedef struct dictEntry //实际存储对象,一个链表结构,主要包括`key`,`value`和`next`指针,以及一个 union结构用来存储额外的数据。
相关源文件:dict.h,dict.c
有兴趣可以直接看源码,分别在dict.h他dict.c中。这里不详细展开,这次我们不会自己实现dict结构,而是直接使用Golang中的map,后续在实现数据结构的部分时再详细说明。
dict.go
type dict map[interface{}]interface{}
//在字段中查找元素
func (d dict) dictFind(key interface{}) interface{} {}
//新增元素,如果已存在,则返回err, 成功添加,则返回ok
func (d dict) dictAdd(key interface{}, val interface{}) int{}
//从dict中替换元素,如果已存在,则替换,并返回0
//如果不存在,则新增,返回1
func (d dict) dictReplace(key interface{}, val interface{}) int{}
//从dict中删除元素
func (d dict) dictDelete(key interface{}) {}
robj实现
Redis中还有一个比较重要的数据结构robj,它是一个通用的数据结构,实际上可以指向不同的数据结构,有一点类似Java中Object对象(实际上并不是),robj的定义:
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
typedef struct redisObject {
unsigned type:4; //数据类型,上面define的5种
unsigned encoding:4; //编码方式
unsigned lru:REDIS_LRU_BITS; //lru时间
int refcount; //引用计数,某些情况下robj对象可以被共享
void *ptr; //指向实现对象的底层数据结构
} robj;
相关源文件:redis.h,object.c
我们也参考它的结构体,实现我们的robj:
object.go
type robj struct {
rtype uint8
encoding uint8
lru uint32
refcount int
ptr interface{}
}
//创建robj对象
func createObject(t uint8, ptr interface{}) *robj{
return &robj {
rtype: t,
encoding: 0,
refcount: 1,
ptr: ptr,
lru: lruClock(),
}
}
list实现
还有一个list,我们这次不会使用到,不过也可以先定义出来:
rlist.go
type rlist list.List
func (l *rlist) Init() *rlist {
lt := (*list.List)(l).Init()
return (*rlist)(lt)
}
其它
其它的数据结构我们这次完全不会涉及到,所以这次就先忽略,后续用到再来实现即可。
DB实现
接下来就是存储部分,在Redis中主要使用一个redisDb的结构进行数据的存储,里面包含两个字典:dict和expires,分别用来存储 key-value数据和key的过期时间,这里就不再过Redis的源码(db.c)了,我们直接看我们自己定义的结构:
db.go
//存储数据结构
type redisDb struct{
dict *dict //dict,用来存储k-v数据, key = *robj, value = *robj
expires *dict //expires,用来存储带有过期时间的键, key = *robj, value = timestamp
id int //id
}
func (r *redisDb) setKey(key *robj, val *robj){
if r.lookupKey(key) == nil {
r.dbAdd(key, val)
}else {
//override
r.dbOverwrite(key, val)
}
val.refcount++
}
func (r *redisDb) lookupKey(key *robj) *robj {
//TODO expire if needed
return r.doLookupKey(key)
}
func (r *redisDb) setExpire(key *robj, expire uint64) {
kde := r.dict.dictFind(key.ptr)
if kde != nil {
r.expires.dictAdd(key.ptr, expire)
}
}
服务端实现
Redis的单机服务端主要的redis.h和redis.c中实现,其中最核心的是redisServer、redisClient和redisCommand这三个结构。
redisServer主要保存了服务端的各种配置(包括网络、持久化、集群等等配置信息)、维护存储结构(db),维护Redis服务端的网络相关的状态和数据(客户端连接、fd、eventloop等)、统计数据等等。redisClient主要是用来维护客户端连接信息,包括db、fd、请求相关数据、响应相关数据等一系列数据与状态。redisCommand则主要是保存了Redis的命令名称以及对应的命令处理器。
redis.go
//Redis服务端结构
type redisServer struct {
pid int //pid
db *redisDb //db
commands *dict //Redis命令字典,key = sds(命令,比如get/set), value = *redisCommand
clientCounter int //存储client的id计数器
clients *dict //客户端字典, key = id, value = *redisClient
port int //端口
tcpBacklog int
bindaddr string //地址
ipfdCount int
events *eventloop //事件处理器
lruclock uint32
}
type redisClient struct {
id int
conn gnet.Conn //客户端连接
db *redisDb //db
name *robj
argc int //命令数量
argv []*robj //命令值
cmd *redisCommand //当前执行的命令
lastcmd *redisCommand //最后执行的命令
reqtype int //请求类型
queryBuf sds //从客户端读到的数据
buf []byte //准备发回给客户端的数据
bufpos int //发回给客户端的数据的pos
sentlen int //已发送的字节数
}
//reids命令结构
type redisCommand struct {
name sds //命令名称
redisCommandFunc func(client *redisClient) //命令处理函数
}
实际上Redis中对
redisServer和redisClient这两个结构还有大量的定义,特别是redisServer,这里忽略了大部分,暂时只关心我们目前需要的。
接下来是redisServer的初始化,主要分为初始化配置和初始化server:
redis.go
//初始化server配置
func initServerConfig() {
server.port = redisServerPort
server.tcpBacklog = redisTcpBacklog
server.events = &eventloop{}
populateCommandTable()
}
//初始化server
func initServer() {
server.pid = os.Getpid()
server.clients = &dict{}
//初始化事件处理器
server.events.react = dataHandler
server.events.accept = acceptHandler
//if server.ipfd == nil {
// os.Exit(1)
//}
//初始化db
server.db = &redisDb{
dict: &dict{},
expires: &dict{},
id: 1,
}
createSharedObjects()
}
还有命令的通用处理逻辑也在这里实现(这里在下面再详细说明):
redis.go
//处理命令
func processCommand(client *redisClient) int {}
在processCommand时,redisClient中已经有了解析后的参数数据,这里只需要处理就行了。而数据的接收、发送、客户端的创建以及协议的解析,则是在网络层的实现。
networking实现
首先我们先看一下Redis的事件模型,它没有使用libevent库,而是自己实现了一套基于reactor模型的事件模型:typedef struct aeEventLoop{},它支持epoll、select、kqueue和基于Solaris的event ports,主要提供了两种事件类型的驱动:IO事件(包括IO的读/写事件)和定时器事件(一次性定时器和循环定时器)。
这块我们不自己实现,选择使用开源框架gnet,一个基于Event-Loop事件驱动的网络库,底层也是使用epoll和kqueue系统调用。有兴趣可以直接上github了解。
eventloop.go
type eventloop struct {
react func(frame []byte, c gnet.Conn) (out []byte, action gnet.Action)
accept func(c gnet.Conn) (out []byte, action gnet.Action)
*gnet.EventServer
}
//读事件处理
func (e *eventloop) React(frame []byte, c gnet.Conn) (out []byte, action gnet.Action) {
return e.react(frame, c)
}
//新连接处理
func (e *eventloop) OnOpened(c gnet.Conn) (out []byte, action gnet.Action) {
return e.accept(c)
}
//启动
func elMain() {
addr := "tcp://" + server.bindaddr+":"+ strconv.Itoa(server.port)
log.Printf("listening at: %s", addr)
err := gnet.Serve(server.events, addr,
gnet.WithMulticore(false), gnet.WithNumEventLoop(1))
if err != nil {
log.Fatal(err)
}
}
这里我们主要关心两个事件,一个是React,也就是读事件,一个OnOpened,新连接被创建事件。具体的处理逻辑在networking.go中,这两个事件回调函数在server初始化时被注册到eventloop中。
networking.go
//接收到新的请求,创建客户端,用来处理命令和回复命令
func acceptHandler(c gnet.Conn) (out []byte, action gnet.Action) {
client := createClient(c)
server.clients.dictAdd(client.id, client)
log.Printf("accept connection, client: %v", client)
return out, action
}
//接收到客户端的命令
func dataHandler(frame []byte, c gnet.Conn) (out []byte, action gnet.Action) {
//找到对应的client对象
client := server.clients.dictFind(c.Context()).(*redisClient)
//将数据设置到client中
client.queryBuf = sds(frame)
defer func() {
if err := recover(); err != nil {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
log.Print(err)
log.Printf("==> %s \n", string(buf[:n]))
}
}()
//处理数据
processInputBuffer(client)
return out, action
}
可以看到逻辑很简单:当连接创建事件触发时,我们创建一个redisClient对象,并放入server中。 当读事件触发时,我们从server中获取对应的redisClient对象,然后执行处理数据的逻辑。
networking.go
//处理客户端收到的数据
func processInputBuffer(client *redisClient) {
//判断命令类型
if client.queryBuf[0] == '*' {
client.reqtype = redisReqMultibulk
}else {
client.reqtype = redisReqInline
}
log.Printf("client reqtype is : %v", client.reqtype)
//协议解析
if client.reqtype == redisReqInline {
if processInlineBuffer(client) != nil {
//error
}
}
if client.reqtype == redisReqMultibulk {
if processMultibulkBuffer(client) == redisErr {
//error
log.Printf("analysis protocol error")
}
}else {
panic("Unknown request type")
}
if client.argc == 0 {
resetClient(client)
}else {
if processCommand(client) ==redisErr {
//error
}
resetClient(client)
//server.currentClient = nil
}
}
处理数据实际上就是解析协议,将解析出来的参数放入redisClient中,用于后续的命令处理。比如我们收到以下数据:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n,稍微格式化一下:
*3
$3
SET
$5
mykey
$7
myvalue
那么在processInputBuffer中我们会把它解析成3个参数,然后设置参数个数:redisClient.argc=3和实际的参数值(一个robj数组):redisClient.argv=[SET,mykey,myvalue]
Redis的协议很简单,可以参考:redis.io/topics/prot…
当命令被处理完成之后,我们需要返回数据给客户端,核心方法是addReply:
networking.go
func addReply(client *redisClient, robj *robj) {
log.Printf("add reply: %v", robj)
addReplyToBuffer(client, robj.ptr.(sds))
sendReplyToClient(client)
}
func addReplyToBuffer(client *redisClient, data sds) {
copy(client.buf[client.bufpos:], data)
client.bufpos = client.bufpos + len([]byte(data))
}
func sendReplyToClient(client *redisClient) int{
log.Printf("send reply to client: %v", client.buf[client.sentlen:client.bufpos])
err := client.conn.AsyncWrite(client.buf[client.sentlen:client.bufpos])
if err != nil {
log.Printf("err: %v", err)
}
client.sentlen = client.bufpos
if client.flags & redisCloseAfterReply == 1 {
freeClient(client)
}
return redisOk
}
逻辑也比较简单,将数据写入redisClient的写出缓冲区,然后通过sendReplyToClient方法读取缓冲区的数据并写回给客户端。
这里为什么要先写入缓冲区这么多些一举? 因为在Redis的实现中,
sendReplyToClient是通过IO写事件的方式被注册到aeEventloop中,由ae来调度的,所以它们的数据需要通过redisClient的缓冲区来传递。
在写数据的时候,其实Redis还有很多设计都比较巧妙,比如它每次发送都有一个上限:REDIS_MAX_WRITE_PER_EVENT = (1024*64),即每次IO写事件发送的字节数最大也只会是64K,这是因为它的单线程设计,为了保证在有大量的数据需要发送时 (比如在处理keys *这样的命令),也可以接收其它的请求。
命令处理
最后我们看一下命令的处理,上面刚刚已经提到过了,通用的命令处理逻辑在server中实现:
redis.go
//处理命令
func processCommand(client *redisClient) int {
if client.argv[0].ptr == "quit" {
client.flags |= redisCloseAfterReply
addReply(client, shared.ok)
return redisErr
}
client.cmd = lookupCommand(client.argv[0].ptr.(sds))
client.lastcmd = client.cmd
if client.cmd == nil {
log.Printf("client is empty,return err")
addReply(client, shared.err)
return redisOk
}
call(client, 0)
return redisOk
}
func lookupCommand(name sds) *redisCommand {
cmd := server.commands.dictFind(name)
log.Printf("lookup command: %v", cmd)
if cmd == nil {
return nil
}
return cmd.(*redisCommand)
}
//Call() is the core of Redis execution of a command
func call(client *redisClient, flag int) {
client.cmd.redisCommandFunc(client)
}
- 处理命令时,如果是quit,则直接回复ok,并设置客户端标记,标识这次回复后,直接释放客户端。
- 然后查找命令,如果不存在,则直接返回err
- 如果找到了对应的命令处理器,则执行
实际的命令处理器保存在commands字典中:
redis.go
var (
redisCommandTable = []*redisCommand{
{sds("get"), getCommand},
{sds("set"), setCommand},
}
)
我们定义了一个全局的命令映射表,这里只实现get和set命令的处理器,它们都属于string类型的命令,因此在string数据类型中进行实现:
t_string.go
//SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>]
//将key,value保存到db.dict中
//如果有设置过期时间,那么在db.expires中也保存
func setCommand(client *redisClient) {
log.Print("starting set command")
var expire *robj = nil
flags := redisSetNoFlag
unit := unitSeconds
for i:=3; i < client.argc; i++ {
c := client.argv[i].ptr.(sds)
var next *robj = nil
if i < client.argc - 1 {
next = client.argv[i + 1]
}
//处理NX XX EX PX
if (c[0] == 'n' || c[0] == 'N') && (c[1] == 'x' || c[1] == 'X') {
flags |= redisSetNx
}else if (c[0] == 'x' || c[0] == 'X') && (c[1] == 'x' || c[1] == 'X') {
flags |= redisSetXx
}else if (c[0] == 'e' || c[0] == 'E') && (c[1] == 'x' || c[1] == 'X') && next != nil {
unit = unitSeconds
expire = next
}else if (c[0] == 'p' || c[0] == 'P') && (c[1] == 'x' || c[1] == 'X') && next != nil {
unit = unitMilliseconds
expire = next
}else {
//命令异常
addReply(client, shared.syntaxerr)
return
}
}
setGenericCommand(client, flags, client.argv[1], client.argv[2], expire, unit)
}
//GET key
func getCommand(client *redisClient) {
getGenericCommand(client)
}
//get命令很简单,直接根据key从db.dict中查询对应的value返回
func getGenericCommand(client *redisClient) int {
o := client.db.lookupKey(client.argv[1])
if o == nil {
addReply(client, shared.ok)
return redisErr
}else {
addReplyBulk(client, o)
return redisOk
}
}
func setGenericCommand(client *redisClient, flags int, key *robj, val *robj, expire *robj, unit uint64) {
var milliseconds uint64 = 0
if expire != nil {
imsstr, _ := strconv.Atoi(string(expire.ptr.(sds)))
milliseconds = uint64(imsstr)
if milliseconds < 0 {
addReply(client, shared.err)
return
}
}
if unit == unitSeconds {
milliseconds = milliseconds * 1000
}
if (flags&redisSetNx > 0 && client.db.lookupKey(key) != nil) ||
(flags&redisSetXx > 0 && client.db.lookupKey(key) != nil) {
addReply(client, shared.nullbulk)
}
client.db.setKey(key, val)
if expire != nil {
//如果存在expire,则在db.expires中添加key
client.db.setExpire(key, mstime()+milliseconds)
}
addReply(client, shared.ok)
}
这两个命令的实现逻辑很简单,set其实就是把键和值存在db.dict字典中,如果同时有设置过期时间,那么也需要db.expires中存一份,只不过value是它的过期时间。get就是从db.dict中查询出对应的值,然后返回给客户端。
启动
到这里基本的功能就已经实现啦!增加启动逻辑,就可以运行我们的服务器了:
redis.go
func Start() {
initServerConfig()
initServer()
elMain()
}
main.go
func main() {
redis.Start()
}
结语
到这里一个简单的Redis服务器就算实现完成了,虽然只是简单的实现了get和set方法,但是这个架子已经搭起来了,后续我们所有的功能都会基于这个核心逻辑来进行扩展,下一篇文章预计会实现Redis的key过期策略。
完整代码: github.com/cadeeper/my…