从零开始实现一个Redis(一)

4,082 阅读12分钟

前言

自己在学习Redis的时候,看过不少文章,也买了不少的书,但是很多知识刚看完,没过几天就忘记了,重复看,重复忘。而且很多东西看了也仅仅只是记住了,但其实并没有真正的理解它。所谓纸上得来终觉浅,绝知此事要躬行,因此决定自己参考Redis的源码,一步一步的去实现一个简易版本的Redis,加深理解。

Redis整个项目还是比较大的,第一阶段我们先将整个核心的框架搭起来,可以执行简单的getset命令,后续在基于这个框架来实现后面的各种功能。

整个项目参考Redis 3.0实现,开发语言使用Golang

本文原始连接:juejin.cn/post/695135…

先看一下我们的实现效果:

start1.gif

command1.gif

项目结构

整个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服务器,并可以进行简单的字符串的getset操作,以及quit退出操作。 很多功能和数据结构我们在这个阶段都会略过或者直接使用Golang中已有的实现作为替代。

下面在实现过程中,如果涉及到一些这次功能没有使用到的逻辑或原理,会暂且略过,我们后面去实现它的时候会详细说明。

数据结构实现

仅仅实现string操作的话,我们只需要实现几个必要的数据结构即可,包括sdsdict以及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.hdict.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的结构进行数据的存储,里面包含两个字典:dictexpires,分别用来存储 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.hredis.c中实现,其中最核心的是redisServerredisClientredisCommand这三个结构。

  • 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中对redisServerredisClient这两个结构还有大量的定义,特别是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{},它支持epollselectkqueue和基于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)
}

  1. 处理命令时,如果是quit,则直接回复ok,并设置客户端标记,标识这次回复后,直接释放客户端。
  2. 然后查找命令,如果不存在,则直接返回err
  3. 如果找到了对应的命令处理器,则执行

实际的命令处理器保存在commands字典中:

redis.go

var (
	redisCommandTable = []*redisCommand{
		{sds("get"), getCommand},
		{sds("set"), setCommand},
	}
)

我们定义了一个全局的命令映射表,这里只实现getset命令的处理器,它们都属于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服务器就算实现完成了,虽然只是简单的实现了getset方法,但是这个架子已经搭起来了,后续我们所有的功能都会基于这个核心逻辑来进行扩展,下一篇文章预计会实现Redis的key过期策略。

完整代码: github.com/cadeeper/my…