关于go-redis的事件循环

关于事件循环机制

项目地址

redis的核心流程就是它的AE事件循环。
事件循环中包含两类事件:文件事件和时间事件。
go-redis跟redis的事件循环机制一样,我管他叫KE

type KeLoop struct {
	FileEvents      map[int]*KeFileEvent
	TimeEvents      *KeTimeEvent
	fileEventFd     int
	timeEventNextId int
	stop            bool
}

在KeLoop这个结构体中,有一个map,用来存文件事件,有一个链表,用来存时间事件。
fileEventFd :这个fd是epoll的fd,我们底层会使用epoll io多路复用
timeEventNextId:用来标记下一个时间事件的id
stop:用来标记是否结束。

redis 1.0版本(后面版本我也不知道)中文件事件和时间事件都是链表
然是我这里不用链表是为啥呢?链表做增删好麻烦。每次查找都要便利。
这里为啥不用我自己实现的dict而用了map呢?因为这个map不会用到特别大的数据量,dict适合大数据量的情况。所以用原生的就好了。

FileEvent

每一个文件事件包含下面这些

type KeFileEvent struct {
	fd    int
	mask  FeType
	proc  FileProc
	extra interface{}
}

fd就是文件描述符
mask就是掩码,用来区分读写两种情况的
proc就是具体执行这个文件事件的函数
extra是额外的数据,其实就是client,client里是我们要读/写数据的对端。

添加和删除

添加fileEvent的函数长这个样子:

// 添加文件事件
func (loop *KeLoop) AddFileEvent(fd int, mask FeType, proc FileProc, extra interface{}) {
	// 拿到epoll ctl
	ev := loop.getEpollMask(fd)
	if ev&fe2ep[mask] != 0 {
		// 如果事件已经注册过
		return
	}
	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("epoll ctl error: %v\n", err)
		return
	}

	// 添加到事件循环中去。
	var fe KeFileEvent
	fe.fd = fd
	fe.mask = mask
	fe.proc = proc
	fe.extra = extra
	loop.FileEvents[getFeKey(fd, mask)] = &fe
	log.Printf("ke add file event fd:%v, mask:%v\n", fd, mask)
}

其本质就是把fd注册到epoll中,然后再将事件注册到事件循环中去。

函数getEpollMask:用于判断是否已经注册过 读/写事件。

const (
	KE_READABLE FeType = 1
	KE_WRITABLE FeType = 2
)

var fe2ep = [3]uint32{0, unix.EPOLLIN, unix.EPOLLOUT}

// 根据类型,确定一个Fe的Key   fd+mask 确定唯一的一个FileEvent
func getFeKey(fd int, mask FeType) int {
	if mask == KE_READABLE {
		return fd
	} else {
		return fd * -1
	}
}

func (loop *KeLoop) getEpollMask(fd int) uint32 {
	var ev uint32
	// 如果已经注册过读事件了
	if loop.FileEvents[getFeKey(fd, KE_READABLE)] != nil {
		ev |= fe2ep[KE_READABLE]
	}
	if loop.FileEvents[getFeKey(fd, KE_WRITABLE)] != nil {
		ev |= fe2ep[KE_WRITABLE]
	}
	return ev
}

如果注册过的事件,和现在要注册的事件一样,就会进入以下分支,那么就结束。

	if ev&fe2ep[mask] != 0 {
		// 如果事件已经注册过
		return
	}

如果还没有注册过,那么就通过epollCtl来注册进去。

而getFeKey这个函数的最用是区分同一个fd下的读事件和写事件。

删除的过程是这样的

// 移除文件事件
func (loop *KeLoop) RemoveFileEvent(fd int, mask FeType) {
	// epoll ctl
	op := unix.EPOLL_CTL_DEL
	ev := loop.getEpollMask(fd)
	ev &= ^fe2ep[mask] // 取反再与,把操作摘出来
	if ev != 0 {
		op = unix.EPOLL_CTL_MOD
	}
	err := unix.EpollCtl(loop.fileEventFd, op, fd, &unix.EpollEvent{Fd: int32(fd), Events: ev})
	if err != nil {
		log.Printf("epoll del error:%v\n", ev)
	}
	loop.FileEvents[getFeKey(fd, mask)] = nil
	log.Printf("ae remove file event fd:%v,mask:%v\n", fd, mask)
}

把要进行的操作拿出来。
使用EpollCtl进行操作
然后在map里面删掉他。

关于timeEvent

在循环中,timeEvent通过链表来组织。

type KeTimeEvent struct {
	id       int
	mask     TeType
	when     int64
	internal int64
	proc     TimeProc
	extra    interface{}
	next     *KeTimeEvent
}

mask 用来表示事件类型。
internal 表示触发的时间间隔。 单位为毫秒。
when用来表示下次触发是啥时候。

采用头插法进行插入。

func (loop *KeLoop) AddTimeEvent(mask TeType, interval int64, proc TimeProc, extra interface{}) int {
	id := loop.timeEventNextId
	loop.timeEventNextId++
	var te KeTimeEvent
	te.id = id
	te.mask = mask
	te.internal = interval
	te.when = GetMsTime() + interval
	te.proc = proc
	te.extra = extra
	te.next = loop.TimeEvents // 采用头插法
	loop.TimeEvents = &te
	return id
}

GetMsTime() 用来获取当前的毫秒时间。

主流程是啥?

以下就是主流程,其实就是一个无限循环。

func (loop *KeLoop) KeMain() {
	for loop.stop != true {
		tes, fes := loop.KeWait()
		loop.KeProcess(tes, fes)
	}
}

KeWait拿到所有就绪的fileEvent和timeEvent
KeProcess处理所有的event。

KeWait:

/*
*
等待过程
1. epoll 等连接,设置一个timeout, 这个timeout的长度就是距离下一个timeEvent最近的时间,这样不会耽误timeEvent的时间.
2. 从epoll_wait中拿到fileEvent 的fd 然后根据事件类型,拿到key,再通过key收集fileEvent
3. 遍历链表获取timeEvent
4. 将两类事件返回
*/
func (loop *KeLoop) KeWait() (tes []*KeTimeEvent, fes []*KeFileEvent) {
	timeout := loop.nearestTime() - GetMsTime()
	if timeout <= 0 {
		timeout = 10
	}
	var events [128]unix.EpollEvent
	n, err := unix.EpollWait(loop.fileEventFd, events[:], int(timeout)) // 如果timeout时间内还没有就绪,就要返回了,不能耽误TimeEvent
	if err != nil {
		log.Printf("epoll wait warnning: %v\n", err)
	}
	if n > 0 {
		log.Printf("ae get %v epoll events\n", n)
	}

	// 收集FileEvent
	for i := 0; i < n; i++ {
		if events[i].Events&unix.EPOLLIN != 0 {
			fe := loop.FileEvents[getFeKey(int(events[i].Fd), KE_READABLE)]
			if fe != nil {
				fes = append(fes, fe)
			}
		}
		if events[i].Events&unix.EPOLLOUT != 0 {
			fe := loop.FileEvents[getFeKey(int(events[i].Fd), KE_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
}

这段代码的意义是设置一个timeout,这个timeout是对fileEvent的等待时间。这个等待时间不能大于距离下一个最近的timeEvent的时间。

	timeout := loop.nearestTime() - GetMsTime()
	if timeout <= 0 {
		timeout = 10
	}

如果有事件就绪,就会将就绪的fd放到events中。

n, err := unix.EpollWait(loop.fileEventFd, events[:], int(timeout)) 

然后一个循环将所有的fileEvent收集起来。

之后就是timeEvent
一个遍历链表的常规操作。

posted @ 2023-09-05 14:18  博客是个啥?  阅读(9)  评论(0编辑  收藏  举报