geektutu / blog

极客兔兔的博客,Coding Coding 创建有趣的开源项目。
https://geektutu.com
Apache License 2.0
166 stars 21 forks source link

动手写分布式缓存 - GeeCache第二天 单机并发缓存 | 极客兔兔 #64

Open geektutu opened 4 years ago

geektutu commented 4 years ago

https://geektutu.com/post/geecache-day2.html

7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了 sync.Mutex 互斥锁的使用,并发控制 LRU 缓存。实现 GeeCache 核心数据结构 Group,缓存不存在时,调用回调函数(callback)获取源数据。

xiaoxfan commented 4 years ago

秒啊

mcrwayfun commented 4 years ago

定义一个函数类型 F,并且实现接口 A 的方法,然后在这个方法中调用自己。这是 Go 语言中将其他函数(参数返回值定义与 F 一致)转换为接口 A 的常用技巧。

Question: 想问下这种有哪些应用场景?有文章介绍看下吗

geektutu commented 4 years ago

@mcrwayfun 这个语法并没有什么特别的。你其实可以反过来想一想,如果不提供这个把函数转换为接口的函数,你调用时就需要创建一个struct,然后实现对应的接口,创建一个实例作为参数,相比这种方式就麻烦得多了。

mcrwayfun commented 4 years ago

@geektutu @mcrwayfun 这个语法并没有什么特别的。你其实可以反过来想一想,如果不提供这个把函数转换为接口的函数,你调用时就需要创建一个struct,然后实现对应的接口,创建一个实例作为参数,相比这种方式就麻烦得多了。

确实是这样,感谢博主解答

walkmiao commented 4 years ago

@mcrwayfun 定义一个函数类型 F,并且实现接口 A 的方法,然后在这个方法中调用自己。这是 Go 语言中将其他函数(参数返回值定义与 F 一致)转换为接口 A 的常用技巧。

Question: 想问下这种有哪些应用场景?有文章介绍看下吗

http包中的HandlerFunc类型就是这种做法,本身这个类型实现了ServeHTTP方法。然后用作对其他普通函数的包装

walkmiao commented 4 years ago

这个group的load 和 getlocally 只保留一个就好了啊 没必要搞两个参数和返回值一模一样的group方法吧

geektutu commented 4 years ago

@walkmiao 分布式场景下,load 会先从远程节点获取 getFromPeer,失败了再回退到 getLocally,设计时预留了。

walkmiao commented 4 years ago

@geektutu @walkmiao 分布式场景下,load 会先从远程节点获取 getFromPeer,失败了再回退到 getLocally,设计时预留了。

还没看到后面 不好意思啊 学到很多 谢谢分享

terrysco commented 4 years ago

return v.(ByteView), 没看懂这个

geektutu commented 4 years ago

@terrysco 是不是应该先学习下 go 的基础语法?

yangzhuangqiu commented 4 years ago

两个疑问:

  1. cache为啥不用读写锁
    type cache struct {
    mu         sync.Mutex
    lru        *lru.Cache
    cacheBytes int64
    }
  2. 结构体传值是深拷贝,cloneBytes是否多余
    func (g *Group) getLocally(key string) (ByteView, error) {
    bytes, err := g.getter.Get(key)
    if err != nil {
        return ByteView{}, err
    }
    value := ByteView{b: cloneBytes(bytes)}
    g.populateCache(key, value)
    return value, nil
    }
walkmiao commented 4 years ago

@yangzhuangqiu 两个疑问:

  1. cache为啥不用读写锁
    type cache struct {
    mu         sync.Mutex
    lru        *lru.Cache
    cacheBytes int64
    }
  2. 结构体传值是深拷贝,cloneBytes是否多余
    func (g *Group) getLocally(key string) (ByteView, error) {
    bytes, err := g.getter.Get(key)
    if err != nil {
      return ByteView{}, err
    }
    value := ByteView{b: cloneBytes(bytes)}
    g.populateCache(key, value)
    return value, nil
    }

我觉得你说的 都是对的一些细节可能没注意

geektutu commented 4 years ago

@yangzhuangqiu

  1. cache 的 getadd 都涉及到写操作(LRU 将最近访问元素移动到链表头),所以不能直接改为读写锁。如果 cache 侧和 LRU 侧同时使用锁细颗粒度控制,是有优化空间的,可以尝试下。geecache 那么实现,是为了层层递进,简洁起见。

  2. 防止缓存值被外部程序修改。bytes 是切片,切片不会深拷贝,切片[]byte与byte数组[4]byte是不同的。

walkmiao commented 4 years ago

@geektutu @yangzhuangqiu

  1. cache 的 getadd 都涉及到写操作(LRU 将最近访问元素移动到链表头),所以不能直接改为读写锁。如果 cache 侧和 LRU 侧同时使用锁细颗粒度控制,是有优化空间的,可以尝试下。geecache 那么实现,是为了层层递进,简洁起见。

  2. 防止缓存值被外部程序修改。bytes 是切片,切片不会深拷贝,切片[]byte与byte数组[4]byte是不同的。

后知后觉啊 不动脑子思考的结果

yangzhuangqiu commented 4 years ago

那这样的话,Group的Get方法的返回是不是有问题

    if v, ok := g.mainCache.get(key); ok {
        log.Println("[GeeCache] hit")
        return v, nil
    }

应该改也成这样?

    if v, ok := g.mainCache.get(key); ok {
        log.Println("[GeeCache] hit")
        return ByteView{b:cloneBytes(v.b)}, nil
    }
walkmiao commented 4 years ago

那这样的话,Group的Get方法的返回是不是有问题

  if v, ok := g.mainCache.get(key); ok {
      log.Println("[GeeCache] hit")
      return v, nil
  }

应该改也成这样?

  if v, ok := g.mainCache.get(key); ok {
      log.Println("[GeeCache] hit")
      return ByteView{b:cloneBytes(v.b)}, nil
  }

不需要吧 得到的就是一个ByteView副本啊

geektutu commented 4 years ago

@yangzhuangqiu @walkmiao

保存时复制一份,是担心外部程序仍旧持有切片的控制权,保存后,切片被外部程序修改。

获取时,即调用 get() 时,不需要复制,ByteView 是只读的,不可修改。通过 ByteSlice()String() 方法取到缓存值的副本。只读属性,是设计 ByteView 的主要目的之一。

// ByteSlice returns a copy of the data as a byte slice.
func (v ByteView) ByteSlice() []byte {
    return cloneBytes(v.b)
}
FWangZil commented 4 years ago

大佬3.1是不是错别字了?

不应该,一是数据源的种类太多,没办法一一实现;而是扩展性不好。

是不是该是

不应该,一是数据源的种类太多,没办法一一实现;二是扩展性不好。

geektutu commented 4 years ago

@FWangZil 尽快修正,感谢指出问题~

xenv commented 4 years ago

@geektutu @mcrwayfun 这个语法并没有什么特别的。你其实可以反过来想一想,如果不提供这个把函数转换为接口的函数,你调用时就需要创建一个struct,然后实现对应的接口,创建一个实例作为参数,相比这种方式就麻烦得多了。

调用的时候为什么需要建一个 struct 呢?直接调用函数会有什么问题吗?

type Group struct {
    getter    func(key string) ([]byte, error)
}

func (g *Group) getLocally(key string) (ByteView, error) {
    bytes, err := g.getter(key)
}
geektutu commented 4 years ago

@xenv 直接传函数没有问题,事实上是讨论接口作为参数和函数作为参数的孰优孰劣的问题。即:

NewGroup(name string, cacheBytes int64, getter Getter)
// VS
NewGroup(name string, cacheBytes int64, getterF GetterFunc) 

将某个/某几个方法封装为 interface{},一般是为了获得更好的语义性和通用性。函数作为参数,是固定的,接口作为参数,便于扩展(接口内新增方法)。

在这个地方 GetterFunc 是一个接口型函数,自己是一个函数类型,同时呢实现了接口 Getter。因此,参数 getter 既支持传入实现了接口 Getter 的结构体,也支持直接传入函数(可以被转换为GetterFunc类型)。

很多标准库/开源库都选择传递接口而非函数,比如 net/http 的 Handler,groupcache 的 Getter。

xenv commented 4 years ago

@geektutu @xenv 直接传函数没有问题,事实上是讨论接口作为参数和函数作为参数的孰优孰劣的问题。即:

NewGroup(name string, cacheBytes int64, getter Getter)
// VS
NewGroup(name string, cacheBytes int64, getterF GetterFunc) 

将某个/某几个方法封装为 interface{},一般是为了获得更好的语义性和通用性。函数作为参数,是固定的,接口作为参数,便于扩展(接口内新增方法)。

在这个地方 GetterFunc 是一个接口型函数,自己是一个函数类型,同时呢实现了接口 Getter。因此,参数 getter 既支持传入实现了接口 Getter 的结构体,也支持直接传入函数(可以被转换为GetterFunc类型)。

要做成一个函数式接口也有道理,Java 可以通过 lambda 来快速建立一个函数的闭包对象。Go 没有内置这样的机制。

我一开始的疑虑主要是对使用者来说可能是一个负担,因为他可能不能马上知道这样的 trick 可以快速实现一个函数式接口,很有可能必须依赖文档或者自己实现了。当然这样子使用者也有更大的空间控制外部变量而不是依靠闭包机制,因为 Groupcache 里的 Getter 会需要依赖外部数据库。

zhuyanxi commented 4 years ago

有个想法,在支持cache的并发读写那一部分,如果不使用锁,而是改成使用channel来控制是不是会更好一些,使用channel的话并发性能也会更高些。

geektutu commented 4 years ago

@zhuyanxi 一般来说,可以用缓冲区来提高写性能,但会牺牲读性能。很多数据库会做这样的优化,先并发写在缓冲区,查找时,多一步缓冲区的查询。

wuqinqiang commented 4 years ago

咨询一下,这一段

func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
    if getter == nil {
        panic("nil Getter")
    }
    mu.Lock()
    defer mu.Unlock()
    g := &Group{
        name:      name,
        getter:    getter,
        mainCache: cache{cacheBytes: cacheBytes},
    }
    groups[name] = g
    return g
}

不能只是对map加锁吗

    mu.Lock()
    groups[name] = g
   mu.Unlock()
pteric commented 3 years ago
type Group struct {
    name      string
    getter    Getter
    mainCache cache
}

// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)

请问这个结构体中的getter是否可以直接用GetterFunc这个类型,然后直接把回调函数赋值到getter?不太明白把一个函数变成接口类型相比之下的优势

geektutu commented 3 years ago

@wuqinqiang 只需要对 map 加锁,当时觉得 g 的构造不耗时,习惯了使用 defer 语法释放锁,就这么写了。

geektutu commented 3 years ago

@pteric,这个问题其实前面的评论讨论得比较多了。首先,直接使用 GetterFunc 这个类型是没有问题的。

这种写法称之为函数式接口或者接口型函数。不管是使用、可读性、可维护性都会好很多。从使用角度说,@xenv 的评论的最后一点是很清楚的,参数如果是接口,用户有更大的空间控制外部的变量;如果是函数,就很可能需要闭包类似的机制来实现了。

比如用户需要使用外部数据库,如果是接口类型,结构体 DB 可以作为参数赋值给 getter:

type struct DB {
    url string
    connPool ConnPool
    ...
}

func (db *DB) Get (key string) ([]byte, error) {
    conn := db.connPool.get()
    defer db.connPool.put(conn)
    return conn.Exec(...)
}

如果仅仅是一个函数,使用外部变量时,封装性和便利性就会差一些。

总结下:函数式接口兼顾了两者的好处,参数既可以是接口,又可以是函数(因为函数本身也实现了这个接口)。这种写法有一个前提:即接口内部只定义了一个方法。

比较典型的还有 golang 标准库 http 中的 Handler,参数既支持函数类型,也支持实现了 Handler 接口 (只包含ServeHTTP 方法) 的结构体。

我总结了一篇文章 https://geektutu.com/post/7days-golang-q1.html

Jim-Luo commented 3 years ago

TestGet部分里面的NewGroup函数的第三个参数是Getter接口,这里确实可以传一个GetterFunc对象。但是这个对象本身又是一个函数,所以后面加了括号不就变成调用了吗?这部分没看懂。。如果理解成强制转换的话那怎么和调用区分呢,这里不应该单独写一个函数然后实现Getter接口吗?

geektutu commented 3 years ago

@Jim-Luo 强制类型转换,小括号前面是函数类型,比如将函数 f 转换为 GetterFunc 类型:GetterFunc(f)。调用函数,小括号前面是一个函数实例,比如 :

var f GetterFunc = func() {  }
f(argv...)

函数类型是不能直接调用的,函数实例才可以,应该不存在强制转换和调用区分的问题。

zhuliangnan commented 3 years ago

@terrysco return v.(ByteView), 没看懂这个

v.(ByteView) 这个叫做类型断言 if t,ok := v.(string),如果v是string类型的话,ok就是true,t就是string类型v的值;否则ok为false,t就是string类型的初始化 相当于是判断和赋给的结合 如果可以v和string是同类型的 可以直接这样 t := v.(string) 而且注意一下 v必须是接口类型才可以

feixintianxia commented 3 years ago

@geektutu @yangzhuangqiu @walkmiao

保存时复制一份,是担心外部程序仍旧持有切片的控制权,保存后,切片被外部程序修改。

获取时,即调用 get() 时,不需要复制,ByteView 是只读的,不可修改。通过 ByteSlice()String() 方法取到缓存值的副本。只读属性,是设计 ByteView 的主要目的之一。

// ByteSlice returns a copy of the data as a byte slice.
func (v ByteView) ByteSlice() []byte {
  return cloneBytes(v.b)
}

这个我理解为:
用户调用
view, err := gee.Get(k)  //此刻用户仍然修改view里面的数据
然后用户调用
a := view.String() (或 view.ByteSlice()) //获得新的副本 此时用户可以修改a而不用担心会修改缓存里面的数据了。
既然这样, gee.Get(k) 是否可以直接返回一个副本?

wagaru commented 3 years ago

在 add 方法中,判断了 c.lru 是否为 nil,如果不等于 nil 再创建实例。

這裡是不是打錯了

在 add 方法中,判断了 c.lru 是否为 nil,如果等于 nil 再创建实例。

geektutu commented 3 years ago

在 add 方法中,判断了 c.lru 是否为 nil,如果等于 nil 再创建实例。

@wagaru fixed, thanks. fix commit id

Iheidashuai commented 3 years ago

学习了大佬这几个项目 真的是受益良多 没想到 go 还能这么写 太棒了

banzhihang commented 3 years ago

type Group struct { name string getter Getter mainCache cache } 为什么这里cache 不用指针

huaiyuWangh commented 3 years ago

并发场景下 load可能会被重复调用吧

PenguinCats commented 3 years ago

NewGroup 是否需要判断 group 是否已经存在?

DamonGG commented 3 years ago

请问大佬,这种格式的流程图,用的是什么工具呀?

j511002 commented 3 years ago

在go1.16版本中,go priceOnce(100)这个方法在没有加锁的情况下不会再打印出100了,这是go做了并发优化么?

valiner commented 3 years ago

牛牛牛

schinapi commented 3 years ago

读为什么要加锁,就那个只读锁感觉没有必要吧

j511002 commented 2 years ago

可以问一下为何锁是使用的互斥锁而不是读写锁吗? 像缓存这种场景应该是读写锁的性能优势很大吧?

jianghaodong-icel commented 2 years ago

@schinapi 读为什么要加锁,就那个只读锁感觉没有必要吧

读的本质也是并发的操作map,如果不加锁,并发量大可能会panic

Nengry commented 2 years ago

@schinapi 读为什么要加锁,就那个只读锁感觉没有必要吧

保证读操作和其他并发的写操作不冲突

whoisapig commented 2 years ago

为什么不在接受操作指令执行前,通过 channe 限制处理的单线程 而是通过锁来做并发啊?

018429 commented 2 years ago
qq71719141 commented 2 years ago

定义interface的函数时为什么需要给参数命名呢(Getter.Get 的这个key)

018429 commented 2 years ago
for k, v := range db {
        if view, err := gee.Get(k); err != nil || view.String() != v {
            t.Fatal("failed to get value of Tom")
        } // load from callback function
        if _, err := gee.Get(k); err != nil || loadCounts[k] > 1 {
            t.Fatalf("cache %s miss", k)
        } // cache hit
    }

第一个if里面t.Fatal是否改为t.Fatalf("failed to get value of %s", k)?

zh1C commented 2 years ago

请问一下,groups的设置,感觉没有使用到呀,既然有多个Group,Get()方法还是查找特定key,而不是查找指定group的特定key。