golang:从零开始手写godis,深层理解redis底层
- 回顾
- 回顾redis中的数据结构
- 回顾redis中的io网络模型
- 分析流程
- 先看看我项目中的结构
- 通过main函数分析要实现哪些内容
- ae文件
- 常量定义及数据结构设计
- AddFileHandler
- AddTimeEvent
- AeMain
- Aewait
- AeProcess
- obj文件
- List文件
- dict文件
- dict的结构
- dict的创建、
- rehash
- net文件
- godis文件
- 各种结构
- 常量部分
- server结构
- client结构
- initServer
- tcpAddceptHandler
- ReadQueryFromClient
- SendReplyToClient
- 至此大体流程就结束了,还有一些工作在后面慢慢完善,跑起来试一下
本篇从一个初学者的视角手写redis,如有纰漏请各位大佬指出
回顾
回顾redis中的数据结构
主要有这五种,string,list,set,zset,hash
数据结构方面,本文并不着重讨论。
回顾redis中的io网络模型
redis官方主要提供了linux系统下的版本,而linux版本下io多路复用机制是epoll,本文也只实现linux系统下的epoll
详细的epoll解析可以参考这位大佬的
https://blog.csdn.net/JMW1407/article/details/107963618
好接下来我们开始吧!
分析流程
先看看我项目中的结构
其中godis中的main函数是整个程序的入口
通过main函数分析要实现哪些内容
func main() {
//这里是获取配置文件路径,为了方便测试我直接写死了
//path := os.Args[1]
path := "./config.json"
config, err := LoadConfig(path)
if err != nil {
log.Printf("load config err:%v\n", err)
}
//初始化server
err = initServer(config)
if err != nil {
log.Printf("init server err:%v\n", err)
}
//注册监听事件
server.aeLoop.AddFileEvent(server.fd, AE_READABLE, AcceptHandler, nil)
//注册定时事件
server.aeLoop.AddTimeEvent(AE_NORMAL, 100, ServerCron, nil)
log.Println("godis server is up...")
server.aeLoop.AeMain()
}
其中主要涉及initServer,AcceptHander,ServerCron三个函数
下面我简要阐述一下这三个函数分别是做什么的
1,initServer顾名思义就是初始化一个server,其中主要做了俩件事:一,创建AeLoop。二,创建一个tcp监听端口
2,AcceptHandler是fileEvent的回调函数,处理客户端连接,这里我想引用黑马redis原理篇的ppt,如下
就是右上角tcpAcceptHandler
3,serverCron并不在上图中,熟悉redis的人知道,redis对于过期键的处理并不是立即删除的,而是定时检查一批过期键删除,或者在查询到过期键时删除,详细见
https://blog.csdn.net/weixin_42201180/article/details/129150967
这里的这个函数就是定期删除key的
后续这三个函数还回在godis文件中详细说明
接下来我们看看具体实现
ae文件
这里会用到"golang.org/x/sys/unix"这个包,其中主要封装了一些系统调用
常量定义及数据结构设计
type FeType int
const (
AE_READABLE FeType = 1
AE_WRITABLE FeType = 2
)
type TeType int
const (
AE_NORMAL TeType = 1
AE_ONCE TeType = 2
)
type FileProc func(loop *AeLoop, fd int, extra interface{})
type TimeProc func(loop *AeLoop, fd int, extra interface{})
type AeFileEvent struct {
fd int //文件描述符
mask FeType //记录事件类型,可读,可写,可读写?
proc FileProc //回调函数,如刚才看到的AcceptHandler
extra interface{} //go中用空接口实现的泛型效果
}
type AeTimeEvent struct {
id int
mask TeType //重复执行?还是只执行一次?
when int64 //ms
interval int64 //ms //重复执行的事件间隔
proc TimeProc //同上
extra interface{}
next *AeTimeEvent
}
type AeLoop struct {
FileEvents map[int]*AeFileEvent
TimeEvents *AeTimeEvent
fileEventFd int //文件描述符
timeEventNextId int
stop bool //是否停止程序
}
//用于将自定义的文件事件类型映射到 epoll 的事件类型
var fe2ep [3]uint32 = [3]uint32{0, unix.EPOLLIN, unix.EPOLLOUT}
AddFileHandler
func (loop *AeLoop) AddFileEvent(fd int, mask FeType, proc FileProc, extra interface{}) {
//epoll ctl
ev := loop.getEpollMask(fd)
if ev&fe2ep[mask] != 0 {
//event is registered
return
}
//这里的情况是如果同一个fd下有读,添加写或有写,添加读就modify,完全没注册就add操作
op := unix.EPOLL_CTL_ADD
if ev != 0 {
op = unix.EPOLL_CTL_MOD
}
ev |= fe2ep[mask]
//添加或修改读写
err := unix.EpollCtl(loop.fileEventFd, op, fd, &unix.EpollEvent{Fd: int32(fd), Events: ev})
if err != nil {
log.Printf("EpollCtl(%d,%d): %v", fd, mask, err)
return
}
//添加新事件
var fe AeFileEvent
fe.fd = fd
fe.mask = mask
fe.proc = proc
fe.extra = extra
loop.FileEvents[getFeKey(fd, mask)] = &fe
log.Printf("ae add file event fd:%v,mask:%v\n", fd, mask)
}
这里贴出其中调用的函数
func getFeKey(fd int, mask FeType) int {
//可读+ 反之- 简化流程
if mask == AE_READABLE {
return fd
} else {
return fd * -1
}
}
func (loop *AeLoop) getEpollMask(fd int) uint32 {
var ev uint32
if loop.FileEvents[getFeKey(fd, AE_READABLE)] != nil {
ev |= fe2ep[AE_READABLE]
}
if loop.FileEvents[getFeKey(fd, AE_WRITABLE)] != nil {
ev |= fe2ep[AE_WRITABLE]
}
return ev
}
我来解释一下其中的几个位运算
首先得知道
var fe2ep [3]uint32 = [3]uint32{0, unix.EPOLLIN, unix.EPOLLOUT}
unix.EPOLLIN = 1,unix.EPOLLOUT = 100(二进制下)
用 |= 可以表示可读可写可读写三种情况
ev & fe2ep[mask] !=说明该文件描述符已经有对应的事件所以op是mod不是add
AddTimeEvent
func GetMsTime() int64 {
return time.Now().UnixNano() / 1e6
}
func (loop *AeLoop) AddTimeEvent(mask TeType, interval int64, proc TimeProc, extra interface{}) int {
id := loop.timeEventNextId
loop.timeEventNextId++
var te AeTimeEvent
te.id = id
te.mask = mask
te.interval = interval
te.when = GetMsTime() + interval
te.proc = proc
te.extra = extra
te.next = loop.TimeEvents
loop.TimeEvents = &te
return id
}
介于篇幅有限remove的相关逻辑我就先不贴出来了
AeMain
func (loop *AeLoop) AeMain() {
for loop.stop != true {
tes, fes := loop.AeWait()
loop.AeProcess(tes, fes)
}
}
简单来说
其中AeWait是等待事件发生
AeProcess是具体事件调用各自的回调函数
具体逻辑如下
Aewait
func (loop *AeLoop) AeWait() (tes []*AeTimeEvent, fes []*AeFileEvent) {
timeout := loop.nearestTime() - GetMsTime()
if timeout <= 0 {
timeout = 10
}
var events [128]unix.EpollEvent
n, err := unix.EpollWait(loop.fileEventFd, events[:], int(timeout))
//忽略系统中断
if err != nil && err != unix.EINTR {
log.Printf("EpollWait Warnning: %v", err)
}
if n > 0 {
log.Printf("ae get %v epoll events", n)
}
for i := 0; i < n; i++ {
if events[i].Events&unix.EPOLLIN != 0 {
fe := loop.FileEvents[getFeKey(int(events[i].Fd), AE_READABLE)]
if fe != nil {
fes = append(fes, fe)
}
}
if events[i].Events&unix.EPOLLOUT != 0 {
fe := loop.FileEvents[getFeKey(int(events[i].Fd), AE_WRITABLE)]
if fe != nil {
fes = append(fes, fe)
}
}
}
now := GetMsTime()
p := loop.TimeEvents
for p != nil {
if p.when < now {
tes = append(tes, p)
}
p = p.next
}
return
}
AeProcess
func (loop *AeLoop) AeProcess(tes []*AeTimeEvent, fes []*AeFileEvent) {
for _, te := range tes {
te.proc(loop, te.id, te.extra)
if te.mask == AE_ONCE {
loop.RemoveTimeEvent(te.id)
} else {
te.when = GetMsTime() + te.interval
}
}
if len(fes) > 0 {
log.Println("ae is processing file events")
for _, fe := range fes {
fe.proc(loop, fe.fd, fe.extra)
}
}
}
obj文件
Gobj结构
type Gtype uint8
const (
GSTR Gtype = 0x00
GLIST Gtype = 0x01
GSET Gtype = 0x02
GZSET Gtype = 0x03
GDICT Gtype = 0x04
)
type Gval interface{}
type Gobj struct {
//类型
Type_ Gtype
//具体的值
Val_ Gval
//引用计数,计数为0时释放空间
refCount int
}
List文件
我的实现就是简单的list,这里就不展开说了
dict文件
dict因为是涉及到渐进式hash的,所以这里展开讲讲
dict的结构
type Entry struct {
Key *Gobj
Val *Gobj
next *Entry
}
type htable struct {
table []*Entry
size int64
mask int64
used int64
}
type DictType struct {
//用于计算hash值的函数
HashFunc func(key *Gobj) int64
//用于比较值的函数
EqualFunc func(key1, key2 *Gobj) bool
}
type Dict struct {
DictType
//俩个table是为了方便迁移
hts [2]*htable
//记录rehash的进度,创建时为—1
rehashidx int64
}
dict的创建、
func DictCreate(dictType DictType) *Dict {
var dict Dict
dict.DictType = dictType
dict.rehashidx = -1
return &dict
}
rehash
func (dict *Dict) isRehashing() bool {
return dict.rehashidx != -1
}
func (dict *Dict) rehashStep() {
//TODO check iterators
dict.rehash(DEFAULT_STEP)
}
//判断是否需要扩容
func (dict *Dict) expandIfNeeded() error {
//正在扩容不需要再判断了
if dict.isRehashing() {
return nil
}
//刚创建的dict
if dict.hts[0] == nil {
return dict.expend(INIT_SIZE)
}
//可以扩容的情况
if (dict.hts[0].used > dict.hts[0].size) && (dict.hts[0].used/dict.hts[0].size > FORCE_RATIO) {
return dict.expend(dict.hts[0].size * GROW_RATIO)
}
return nil
}
func nextPower(size int64) int64 {
//寻找合适的扩容后的大小
for i := INIT_SIZE; i < math.MaxInt64; i *= 2 {
if i >= size {
return i
}
}
return -1
}
func (dict *Dict) expend(size int64) error {
sz := nextPower(size)
//虽然正常来说满足以下条件的不会执行expand的函数
//因为在expandIfNeed那一步就过滤了这个情况
//为了代码的健壮性这里还是判断一下
if dict.isRehashing() || (dict.hts[0] != nil && dict.hts[0].size >= sz) {
return EX_ERR
}
//为rehash后的table分配空间
var ht htable
ht.size = sz
ht.mask = sz - 1
ht.table = make([]*Entry, sz)
ht.used = 0
//检查是否初始化
if dict.hts[0] == nil {
dict.hts[0] = &ht
return nil
}
//开始rehash
dict.hts[1] = &ht
dict.rehashidx = 0
return nil
}
func (dict *Dict) rehash(step int) {
//step是渐进式rehash每次迁移的元素的个数
for step > 0 {
//证明原表中没有了数据
if dict.hts[0].used == 0 {
dict.hts[0] = dict.hts[1]
dict.hts[1] = nil
dict.rehashidx = -1
return
}
for dict.hts[0].table[dict.rehashidx] == nil {
dict.rehashidx++
}
//迁移所有所有元素
entry := dict.hts[0].table[dict.rehashidx]
for entry != nil {
ne := entry.next
idx := dict.HashFunc(entry.Key) & dict.hts[1].mask
entry.next = dict.hts[1].table[idx]
dict.hts[1].table[idx] = entry
dict.hts[0].used -= 1
dict.hts[1].used += 1
entry = ne
}
dict.hts[0].table[dict.rehashidx] = nil
dict.rehashidx += 1
step -= 1
}
}
rehash流程;当在添加一个键值对的时候调用 expendIfNeed(判断是否需要扩容) ——> expand(设置rehash的idx为0开始rehash)——> rehash(结束时将ht[1]挂到ht[0]上,rehashidx重新设为-1)
之后每次执行插入更新删除操作时都会
if dict.isRehashing() {
dict.rehashStep()
}
这就是渐进式rehash
如果正在扩容,查询时也会在俩个ht中遍历
for i := 0; i <= 1; i++ {
idx := h & dict.hts[i].mask
e := dict.hts[i].table[idx]
for e != nil {
if dict.EqualFunc(e.Key, key) {
return e
}
e = e.next
}
//只有正在扩容的dict才有俩个ht
if !dict.isRehashing() {
break
}
}
net文件
这里主是对unix包中syscall的再一次封装
import (
"golang.org/x/sys/unix"
"log"
)
const BACKLOG int = 64
func Accept(fd int) (int, error) {
nfd, _, err := unix.Accept(fd)
//ignore client addr for now
return nfd, err
}
func Connect(host [4]byte, port int) (int, error) {
s, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)
if err != nil {
log.Printf("init socket error: %v\n", err)
return -1, err
}
var addr unix.SockaddrInet4
addr.Addr = host
addr.Port = port
err = unix.Connect(s, &addr)
if err != nil {
log.Printf("connect error: %v\n", err)
return -1, err
}
return s, nil
}
func Read(fd int, p []byte) (n int, err error) {
return unix.Read(fd, p)
}
func Write(fd int, p []byte) (n int, err error) {
return unix.Write(fd, p)
}
func Close(fd int) {
unix.Close(fd)
}
func TcpServer(port int) (int, error) {
s, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)
if err != nil {
log.Printf("init socket error: %v\n", err)
return -1, nil
}
err = unix.SetsockoptInt(s, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
log.Printf("set socket error: %v\n", err)
unix.Close(s)
return -1, err
}
var addr unix.SockaddrInet4
//golang.syscall will handle htons
addr.Port = port
err = unix.Bind(s, &addr)
if err != nil {
log.Printf("bind error: %v\n", err)
unix.Close(s)
return -1, err
}
err = unix.Listen(s, BACKLOG)
if err != nil {
log.Printf("listen error: %v\n", err)
unix.Close(s)
return -1, err
}
return s, nil
}
godis文件
这里对着这个ppt看
我这里先说说我的godis中的函数分别对应图上哪里
initServer是注册serverSocket监听
clientSocket由文章开头提到的tcpAddeptHandler负责注册
io多路复用由linux系统实现(这部分在ae包中)
readQueryFromClient,处理命令请求
sendReplyToClient
各种结构
常量部分
const (
COMMAND_UNKNOWN CmdType = 0x00
//处理类似 SET key val 的命令
COMMAND_INLINE CmdType = 0x01
//处理resp协议命令
COMMAND_BULK CmdType = 0x02
)
const (
GODIS_IO_BUF int = 1024 * 16
GODIS_MAX_BULK int = 1024 * 4
GODIS_MAX_INLINE int = 1024 * 4
)
server结构
type GodisDB struct {
//存储键值
data *Dict
//存储键的过期时间
expire *Dict
}
type GodisServer struct {
//监听套接字
fd int
//端口
port int
//存储数据
db *GodisDB
//连接的客户端
clients map[int]*GodisClient
aeLoop *AeLoop
}
client结构
type GodisClient struct {
//连接套接字
fd int
db *GodisDB
//解析命令
args []*Gobj
reply *List
sentLen int
queryBuf []byte
queryLen int
//set get exp?
cmdTy CmdType
//处理RESP命令
bulkNum int
bulkLen int
}
initServer
在函数初始化时调用initServer
func initServer(config *Config) error {
//server.port = config.Port
server.port = 6767
server.clients = make(map[int]*GodisClient)
server.db = &GodisDB{
data: DictCreate(DictType{HashFunc: GStrHash, EqualFunc: GStrEqual}),
expire: DictCreate(DictType{HashFunc: GStrHash, EqualFunc: GStrEqual}),
}
var err error
if server.aeLoop, err = AeLoopCreate(); err != nil {
return err
}
server.fd, err = TcpServer(server.port)
return err
}
//这个在ae包中
func AeLoopCreate() (*AeLoop, error) {
epollFd, err := unix.EpollCreate1(0)
if err != nil {
return nil, err
}
return &AeLoop{
FileEvents: make(map[int]*AeFileEvent),
fileEventFd: epollFd,
timeEventNextId: 1,
stop: false,
}, nil
}
//这个在net包中
func TcpServer(port int) (int, error) {
s, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)
if err != nil {
log.Printf("init socket error: %v\n", err)
return -1, nil
}
err = unix.SetsockoptInt(s, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
if err != nil {
log.Printf("set socket error: %v\n", err)
unix.Close(s)
return -1, err
}
var addr unix.SockaddrInet4
addr.Port = port
err = unix.Bind(s, &addr)
if err != nil {
log.Printf("bind error: %v\n", err)
unix.Close(s)
return -1, err
}
err = unix.Listen(s, BACKLOG)
if err != nil {
log.Printf("listen error: %v\n", err)
unix.Close(s)
return -1, err
}
return s, nil
}
tcpAddceptHandler
func AcceptHandler(loop *AeLoop, fd int, extra interface{}) {
cfd, err := Accept(fd)
if err != nil {
log.Printf("client %v accept err:%v\n", fd, err)
return
}
client := CreateClient(cfd)
//TODO ; check max clients limit
server.clients[cfd] = client
//发现注册客户端
server.aeLoop.AddFileEvent(cfd, AE_READABLE, ReadQueryFromClient, client)
log.Printf("client accept %v\n", cfd)
}
可读事件是为了在客户端发来信息时服务端高敏感的感知,当客户端发送数据时,内核会检测到数据到达接收缓冲区,并通知服务端处理。
可写事件,让内核监听套接字的 可写状态。当内核发送缓冲区有空闲空间时(即网络可以接受新数据),通知服务端写入数据。在后面回复客户端才会注册可写事件
CreateClient就是为client中的字段分配空间并复制
这里传入回调函数的ReadQueryFromClient是读取命令的关键
ReadQueryFromClient
func ReadQueryFromClient(loop *AeLoop, fd int, extra interface{}) {
client := extra.(*GodisClient)
//当没读入回车且小于缓冲区的容量时继续读
if len(client.queryBuf)-client.queryLen < GODIS_MAX_BULK {
client.queryBuf = append(client.queryBuf, make([]byte, GODIS_MAX_BULK)...)
}
n, err := Read(fd, client.queryBuf[client.queryLen:])
if err != nil {
log.Printf("client %v read err %v", fd, err)
//如果出错释放客户端
freeClient(client)
return
}
client.queryLen += n
log.Printf("read %v bytes from client:%v\n", n, client.fd)
log.Printf("ReadQueryFromClient,queryBuf:%x\n", client.queryBuf[:client.queryLen])
//处理读在queryBuf中的内容并执行对应操作
err = ProcessQueryBuf(client)
if err != nil {
log.Printf("process query buf err:%v\n", err)
freeClient(client)
return
}
}
func ProcessQueryBuf(client *GodisClient) error {
for client.queryLen > 0 {
if client.cmdTy == COMMAND_UNKNOWN {
if client.queryBuf[0] == '*' {
//*3
//$3
//set
//$3
//key
//$5
//value
client.cmdTy = COMMAND_BULK
} else {
//set key value
client.cmdTy = COMMAND_INLINE
}
}
var ok bool
var err error
if client.cmdTy == COMMAND_INLINE {
ok, err = handleInlineBuf(client)
} else if client.cmdTy == COMMAND_BULK {
ok, err = handleBulkBuf(client)
} else {
return errors.New("unknown command type")
}
//到现在已经解析出queryBuf中的内容存在c.args中了
if err != nil {
return err
}
if ok {
if len(client.args) == 0 {
//命令无效,重置命令
resetClient(client)
} else {
//处理命令
ProcessCommand(client)
}
} else {
//cmd in completed
break
}
}
return nil
}
func ProcessCommand(c *GodisClient) {
cmdStr := c.args[0].StrVal()
log.Println("process command:", cmdStr)
if cmdStr == "exit" {
freeClient(c)
return
}
//var cmd *GodisCommand
cmd := lookupCommand(cmdStr)
if cmd == nil {
c.AddReplyStr("-ERR: unknown command\r\n")
resetClient(c)
return
} else if cmd.arity != len(c.args) {
c.AddReplyStr("-ERR: wrong number of args\r\n")
resetClient(c)
return
}
//处理命令并将回复加入client.reply中
cmd.proc(c)
//重置命令
resetClient(c)
}
//遍历找到对应的命令
func lookupCommand(cmdStr string) *GodisCommand {
for _, c := range cmdTable {
if c.name == cmdStr {
return &c
}
}
return nil
}
//篇幅有限就举个get命令的例子吧
func getCommand(c *GodisClient) {
key := c.args[1]
val := findKeyRead(key)
if val == nil {
//TODO : extract shared.strings
c.AddReplyStr("$-1\r\n")
} else if val.Type_ != GSTR {
c.AddReplyStr("-ERR: wrong type\r\n")
} else {
str := val.StrVal()
c.AddReplyStr(fmt.Sprintf("$%d\r\n%v\r\n", len(str), str))
}
}
//在AddReplyStr中会注册可写事件
func (c *GodisClient) AddReply(o *Gobj) {
c.reply.Append(o)
o.IncrRefCount()
server.aeLoop.AddFileEvent(c.fd, AE_WRITABLE, SendReplyToClient, c)
}
SendReplyToClient
func SendReplyToClient(loop *AeLoop, fd int, extra interface{}) {
client := extra.(*GodisClient)
log.Printf("send reply to client:%v\n,reply len:%v", client.fd, client.reply.Length())
for client.reply.Length() > 0 {
rep := client.reply.First()
buf := []byte(rep.Val.StrVal())
bufLen := len(buf)
if client.sentLen < bufLen {
n, err := Write(fd, buf[client.sentLen:])
if err != nil {
log.Printf("client send err:%v\n", err)
freeClient(client)
return
}
client.sentLen += n
log.Printf("send %v bytes to client:%v\n", n, client.fd)
if client.sentLen == bufLen {
client.reply.DelNode(rep)
rep.Val.DecrRefCount()
client.sentLen = 0
} else {
break
}
}
}
if client.reply.Length() == 0 {
//client.sentLen = 0
loop.RemoveFileEvent(fd, AE_WRITABLE)
}
}
至此大体流程就结束了,还有一些工作在后面慢慢完善,跑起来试一下
本菜鸟的代码仓库https://github.com/xiaobaicai66695/godis
博主大一,很多不规范的地方还请大家多多包含指正