简介
groupcache是一个分布式kv缓存的library,能够实现对数据的get,而不能进行update和delete操作。groupcache采用的是P2P的架构,所有的节点都是同构的。当客户端程序查找某个值时,groupcache先在本地的cache中进行查找,如果不存在,则通过一致性哈希寻找该key-value所在的peer的地址,如果都不存在则从数据源(数据库)拉取。cache使用LRU策略淘汰最近不常用的数据。需要注意的是,如果很多请求都访问cache中不存在的值时,会导致groupcache向数据源请求数据,这时候需要阻塞请求,保证只有一个请求执行。
需要注意的是groupcache只是一个分布式的kv缓存库,我们需要将自己的规则填入,capotej 实现了一个了一个demo,下面将自顶向下分析整个库的源码。
Demo
客户端与groupcache之间通过RPC通信,而peer之间通过http进行数据的传输,而数据源通过延时模拟一个数据库的存取过程。
架构
Command line client 与前端之间通过RPC进行通信,而groupcache服务器之间通过http进行数据的传输,而DB Server作为数据源,当所有的cache中都不存在的时候,向数据源获取结果
var stringcache = groupcache.NewGroup("SlowDBCache", 64<<20, groupcache.GetterFunc(
func(ctx groupcache.Context, key string, dest groupcache.Sink) error {
result := client.Get(key)
fmt.Printf("asking for %s from dbserver\n", key)
dest.SetBytes([]byte(result))
return nil
}))
上述代码指定了groupcache的名称,大小,以及获取源数据的方法,当客户端程序需要获取某个值时,就会调用Get函数,从groupcache集群中获取,代码如下
func (s *Frontend) Get(args *api.Load, reply *api.ValueResult) error {
var data []byte
fmt.Printf("cli asked for %s from groupcache\n", args.Key)
err := s.cacheGroup.Get(nil, args.Key,
groupcache.AllocatingByteSliceSink(&data))
reply.Value = string(data)
return err
}
LRU实现
代码中的cache可以基于map
和container/list
实现,整个过程非常的简单,具体的实现过程如下:
- 如果一个k-v未在cache中出现,将其加入进
list
和map
中 - 如果这个k-v对已经出现过,则将其放在
list
的头部 - 当
nums >maxEntries
时,将它从list
和map
中移除出去
代码如下
type Cache struct {
MaxEntries int
// 当驱逐某个key时调用的函数,用于回调
OnEvicted func(key Key, value interface{
})
ll *list.List
cache map[interface{
}]*list.Element
}
type Key interface{
}
type entry struct {
key Key
value interface{
}
}
func (c *Cache) Add(key Key, value interface{
}) {
if c.cache == nil {
c.cache = make(map[interface{
}]*list.Element)
c.ll = list.New()
}
if ee, ok := c.cache[key]; ok {
c.ll.MoveToFront(ee)
// ee.Value.(*entry)类型断言成*entry并赋值
ee.Value.(*entry).value = value
return
}
ele := c.ll.PushFront(&entry{
key, value})
c.cache[key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.RemoveOldest()
}
}
删除元素的函数如下所示:首先,从list中删除,然后从map中移除,最后触发onEvicted()
函数
func (c *Cache) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*entry)
delete(c.cache, kv.key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
}
}
一致性哈希
数据的查找使用的是一致性哈希,简单的说就是计算每个key的Hash值,将其存放在hash值大于它的hash值的第一个map中
但是在项目中看不到任何关于迁移的代码,一致性哈希一个重要的特性就是迁移的代价小,增减节点只需要将临近节点的数据迁移
过程如下:
- 首先根据传入的key(
ip+port+id
,id为replica的编号),根据hash函数(提供了默认的hash函数)计算hash值,hash值作为hashMap
的key,key作为value
存储下来,并对key数组排序,方便后续的二分查找 - 在查找某个key所在的server,通过hash值的数组,使用二分法查找第一个hash值大于或等于这个key的hash值的服务器,返回该服务器的
ip+port
代码如下所示
type Hash func(data []byte) uint32
type Map struct {
hash Hash
replicas int
keys []int // Sorted
hashMap map[int]string