关于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
一个遍历链表的常规操作。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)