gnatsd的Subject数据结构及优于mosquitto的原因

主题名(Subject Name)


主题名是大小写敏感的,必须是非空字符串,不能包含空格,使用“.”符号来分层,mqtt中使用“/”分层。
星号“*”匹配一层,跟mqtt中的“+”一样。
大于号“>”匹配后面所有层,跟mqtt中的“#”一样。

源码分析


代码都在sublist.go
先列出数据结构:

// A Sublist stores and efficiently retrieves subscriptions.
type Sublist struct {
    sync.RWMutex
    genid     uint64
    matches   uint64
    cacheHits uint64
    inserts   uint64
    removes   uint64
    cache     map[string]*SublistResult
    root      *level
    count     uint32
}
// A level represents a group of nodes and special pointers to
// wildcard nodes.
type level struct {
    nodes    map[string]*node
    pwc, fwc *node //pwc代表'*'节点,fwc代表'>'节点
}
// A node contains subscriptions and a pointer to the next level.
type node struct {
    next  *level
    psubs []*subscription //普通订阅者列表
    qsubs [][]*subscription //queue订阅者列表
}
// New will create a default sublist
func NewSublist() *Sublist {
    return &Sublist{root: newLevel(), cache: make(map[string]*SublistResult)}
}
// Create a new default level. We use FNV1A as the hash
// algorithm for the tokens, which should be short.
//FNV1A?历史遗留注释吧,这儿分明直接用了golang自带的map哈希。
func newLevel() *level {
    return &level{nodes: make(map[string]*node)}
}

一开始使用NewSublist创建一个Sublist。Sublist保存了所有subject。
Sublist初始化时创建了root节点。


来看看怎么插入一个subject:


// Insert adds a subscription into the sublist
func (s *Sublist) Insert(sub *subscription) error {
    // copy the subject since we hold this and this might be part of a large byte slice.
    subject := string(sub.subject)
    tsa := [32]string{}
    tokens := tsa[:0]
    start := 0
    for i := 0; i < len(subject); i++ {
        if subject[i] == btsep {
            tokens = append(tokens, subject[start:i])
            start = i + 1
        }
    }
    tokens = append(tokens, subject[start:])

    s.Lock()

    sfwc := false
    l := s.root
    var n *node

    for _, t := range tokens {
        lt := len(t)
        if lt == 0 || sfwc { //如果此层长度为0或者上一层已经是'>'了,表示Subject是非法的
            s.Unlock()
            return ErrInvalidSubject
        }

        if lt > 1 { //不是*和>,直接map定位
            n = l.nodes[t]
        } else {
            switch t[0] {
            case pwc:
                n = l.pwc
            case fwc:
                n = l.fwc
                sfwc = true //表示此层只能是最后一层
            default: //不是*和>,直接map定位
                n = l.nodes[t]
            }
        }
        if n == nil { //node节点还没有则创建
            n = newNode()
            if lt > 1 {
                l.nodes[t] = n
            } else {
                switch t[0] {
                case pwc:
                    l.pwc = n
                case fwc:
                    l.fwc = n
                default:
                    l.nodes[t] = n
                }
            }
        }
        if n.next == nil {
            n.next = newLevel()
        }
        l = n.next //下一层
    }
    //上面循环结束后此时n是最后一层的node节点
    if sub.queue == nil { //不是queue,把sub加到psubs中。psubs切片存储了所有订阅此subject的client
        n.psubs = append(n.psubs, sub)
    } else {
        // This is a queue subscription
        if i := findQSliceForSub(sub, n.qsubs); i >= 0 {
            n.qsubs[i] = append(n.qsubs[i], sub)
        } else {
            n.qsubs = append(n.qsubs, []*subscription{sub})
        }
    }

    s.count++
    s.inserts++

    s.addToCache(subject, sub)
    atomic.AddUint64(&s.genid, 1)

    s.Unlock()
    return nil
}

从Insert方法中可以理出整个数据结构:
Sublist第一个层节点是root,root是个level结构,level代表一层。level包含了一个nodes map,nodes存储了此层的所有node,pwc和fwc分别代表了*和> node 。
node的next指向了下一层level,node的psubs存储了普通subject订阅者client,qsubs存储的是queue类别的subject订阅者client。

整个list是个树结构,只不过每层的node节点使用map哈希存储。

先不管Cache干什么用的,先来看看查找匹配:


// matchLevel is used to recursively descend into the trie.
func matchLevel(l *level, toks []string, results *SublistResult) {
    var pwc, n *node
    for i, t := range toks {
        if l == nil {
            return
        }
        if l.fwc != nil { //全匹配,把下面的所有订阅者都加入到results中
            addNodeToResults(l.fwc, results)
        }
        if pwc = l.pwc; pwc != nil { //层匹配,递归子层
            matchLevel(pwc.next, toks[i+1:], results)
        }
        n = l.nodes[t] //查找节点
        if n != nil {  //找到继续下一层
            l = n.next
        } else {
            l = nil
        }
    }
    if n != nil { //找到节点,把订阅者加入到results中
        addNodeToResults(n, results)
    }
    if pwc != nil { //最后一层*通配符的订阅者加入到results中
        addNodeToResults(pwc, results)
    }
}

从指定level比如root开始遍历匹配下面的每一层nodes,如果匹配则把订阅者加入到result中,注意通配符的处理。

每一层使用map快速定位node,使用切片存储此层所有订阅者。
因为使用了map,查询定位比mosquitto的遍历链表树快的多。何况qnatsd还做了Cache。

下面再来看看Cache:


因为查找一个subject的所有订阅者比较费时间,所以使用cache缓存一部分subject订阅者信息,每次查找先去cache中查找,找不到再去sublist中查找,如果找到就加入到cache中,新增一个subject时也要加入到cache中。
cache也有数量限制,当超过一定数量时删除最早的部分cache,防止cache过多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355