groupcache源码中几个有趣的点

简介

groupcache is a caching and cache-filling library, intended as a replacement for memcached in many cases.

groupcache是一个可分布式缓存组件,用于在某些方面替代memcache,不过和一般的缓存有些区别,它只能做get操作(没错,只能get),但是不能做更新和删除操作。另外,groupcache可以很方便的集成到应用程序中,用http接口的形式与其他程序交互。

几个概念

  • peer 代表一个缓存服务器
  • group 缓存以group的概念做组的划分,可以有多个group,每个group有不同的获取数据的方法

几个文件

  • sinks.go 用来装cache的值,可以支持多种类型。

  • consistenthash/consistenthash.go 一个一致性hash的实现

  • http.go 提供peer之间的交互。依赖consistenthash提供的一致性hash的方案,查找key对应的缓存服务器并发出请求,以及提供接口给其他peer访问

  • peers.go 提供了几个和peer相关的方法,如根据groupname获取peer

  • groupcache.go 核心文件,提供获取缓存的方法(先查找本地再查找远端)

  • singleflight/singleflight.go 提供一个竞争执行方法的方法

  • lru/lru.go 顾名思义 ,提供lru的实现

基本使用方式

创建group

// 需要提供一个获取的getter方法
var Group = groupcache.NewGroup("colorCache", 64<<20, groupcache.GetterFunc(
    func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
        log.Println("looking up", key)
        // 这里从数据库中获取一个值 
        v, ok := Store[key]
        if !ok {
            return errors.New("color not found")
        }
        dest.SetBytes(v)
        return nil
    },
))

设置peers并启动服务

// peer是本缓存服务服务的地址 如http://localhost:8080
pool := groupcache.NewHTTPPool(peer)
// ps是一组缓存服务器的地址
pool.Set(ps...)
http.ListenAndServe(*addr, nil)

获取缓存

var b []byte
// 获取缓存结果,存在b中。AllocatingByteSliceSink是一个对获取数据的包装
err := Group.Get(nil, color, groupcache.AllocatingByteSliceSink(&b))

几个有趣的点

peer的查询

给定一个key,groupcache会在本地找不到缓存的情况下,查询该key应该存在的peer。为了在新增或删除peer的时候尽量少的缓存失效,groupcache使用一致性hash的方案,并提供了一个consistenthash的实现,就在consistenthash/consistenthash.go中。

不赘述什么是一致性hash了,直接以注释分析的方式给出代码

type Hash func(data []byte) uint32

// hash 一个hash算法,默认为crc32.ChecksumIEEE
// replicas 每个peer节点会产生几个虚拟节点
// keys 储存所有虚拟节点的hash值
// hashMap 根据hash值找到相应的peer
type Map struct {
    hash     Hash
    replicas int
    keys     []int // Sorted
    hashMap  map[int]string
}

func New(replicas int, fn Hash) *Map {
    m := &Map{
        replicas: replicas,
        hash:     fn,
        hashMap:  make(map[int]string),
    }
    if m.hash == nil {
        m.hash = crc32.ChecksumIEEE
    }
    return m
}

// Returns true if there are no items available.
func (m *Map) IsEmpty() bool {
    return len(m.keys) == 0
}

// Adds some keys to the hash.
func (m *Map) Add(keys ...string) {
    for _, key := range keys {
      // 一个key就是一个peer节点
      // 对于每个key,都会生成replicas个虚拟节点,并把虚拟节点和key的对应关系存起来
        for i := 0; i < m.replicas; i++ {
            hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
            m.keys = append(m.keys, hash)
            m.hashMap[hash] = key
        }
    }
  // 排序 方便后边查找
    sort.Ints(m.keys)
}

// 这边的key为一个cache中的key,即要查询的key
// 计算这个key的hash值,找到与之最为接近的虚拟节点,再通过虚拟节点找到具体的peer
func (m *Map) Get(key string) string {
    if m.IsEmpty() {
        return ""
    }

  // 计算key对应的hash值
    hash := int(m.hash([]byte(key)))

    // 使用二分法查找key对应的hash值最近的虚拟节点
    idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })

    // 一致性hash的虚拟节点应该构成一个环
    if idx == len(m.keys) {
        idx = 0
    }

    return m.hashMap[m.keys[idx]]
}

很经典的实现。

cache的查找过程

简单的说,就是Group.Get(nil, color, groupcache.AllocatingByteSliceSink(&b))发生了什么。

func (g *Group) Get(ctx Context, key string, dest Sink) error {
  // ... (省略一些不重要的代码)
  // 查询本地的缓存,很简单的方法
    value, cacheHit := g.lookupCache(key)

  // 如果命中,统计完直接返回
    if cacheHit {
        g.Stats.CacheHits.Add(1)
        return setSinkView(dest, value)
    }

    // 这边是关键 
    destPopulated := false
    value, destPopulated, err := g.load(ctx, key, dest)
    if err != nil {
        return err
    }
    if destPopulated {
        return nil
    }
    return setSinkView(dest, value)
}

如代码的注释,关键在于在本地没找到缓存的情况下,需要找到合适的peer(可能是自己)去计算出值。这里需要考虑一个问题,就是如果本地有两个协程同时查询一个key怎么办,groupcache会阻塞其中一个,让另一个先跑出结果,避免调用两次获取方法。destPopulated标记该值是不是由该peer新计算出来的(本地方法获取后会直接赋值dest)。

让我们看一下这边的load方法

func (g *Group) load(ctx Context, key string, dest Sink) (value ByteView, destPopulated bool, err error) {
    g.Stats.Loads.Add(1)
  // loadGroup.Do是个竞争的方法,相同的key同时只会有一个访问
    viewi, err := g.loadGroup.Do(key, func() (interface{}, error) {
      // 可能存在两个并发访问了这个方法,后执行的协程可能可以直接从缓存中读入
        if value, cacheHit := g.lookupCache(key); cacheHit {
            g.Stats.CacheHits.Add(1)
            return value, nil
        }
        g.Stats.LoadsDeduped.Add(1)
        var value ByteView
        var err error
      // 根据上边的一致性hash,找到peer
        if peer, ok := g.peers.PickPeer(key); ok {
          // 从peer中获取值
            value, err = g.getFromPeer(ctx, peer, key)
            if err == nil {
                g.Stats.PeerLoads.Add(1)
                return value, nil
            }
            g.Stats.PeerErrors.Add(1)
        }
      // 不需要访问远端peer,可以直接自己获取
        value, err = g.getLocally(ctx, key, dest)
        if err != nil {
            g.Stats.LocalLoadErrs.Add(1)
            return nil, err
        }
        g.Stats.LocalLoads.Add(1)
        destPopulated = true // only one caller of load gets this return value
        // 缓存起来
         g.populateCache(key, value, &g.mainCache)
        return value, nil
    })
    if err == nil {
        value = viewi.(ByteView)
    }
    return
}

还需要解释最后一个问题,如何控制并发的访问。这边使用的是g.loadGroup.Do方法,loadGroup是一个singleflight.Group。让我们看一下这个Do方法做了啥。

package singleflight

import "sync"

// 包装一个key获取值锁需要的一些参数
type call struct {
    wg  sync.WaitGroup
    val interface{}
    err error
}

type Group struct {
    mu sync.Mutex       // protects m
    m  map[string]*call // lazily initialized
}

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    g.mu.Lock()
  // 懒加载
    if g.m == nil {
        g.m = make(map[string]*call)
    }
  // 已经有协程在处理了,阻塞(c.wg.Wait())等待完成
    if c, ok := g.m[key]; ok {
        g.mu.Unlock()
        c.wg.Wait()
        return c.val, c.err
    }
  // 目前没有协程在处理,新建一个处理的任务
    c := new(call)
    c.wg.Add(1)
    g.m[key] = c
    g.mu.Unlock()

    c.val, c.err = fn()
    c.wg.Done()

    g.mu.Lock()
    delete(g.m, key)
    g.mu.Unlock()

    return c.val, c.err
}

这个方法还是比较简单的,主要就是使用sync.WaitGroup来保证只有一个协程在真的调用fn方法获取值,剩下的都在等待获取完成。

小结

看groupcache源码纯粹只是为了学习优秀的go的代码,并没有在实际中使用过… 不过这代码还是很有意思的。

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

推荐阅读更多精彩内容