Open geektutu opened 4 years ago
我有点蒙了
singleflight这样理解对不对: 在一瞬间有大量请求get(key),而且key未被缓存或者未被缓存在当前节点 如果不用singleflight,那么这些请求都会发送远端节点或者从本地数据库读取,会造成远端节点或本地数据库压力猛增。使用singleflight,第一个get(key)请求到来时,singleflight会记录当前key正在被处理,后续的请求只需要等待第一个请求处理完成,取返回值即可。
@xiaoxfan 你的理解是正确的,并发场景下如果 GeeCache 已经向其他节点/源获取数据了,那么就加锁阻塞其他相同的请求,等待请求结果,防止其他节点/源压力猛增被击穿。
当有一个key请求发起后 fn处理函数处理过程中 如果有其他相同key的请求过来 那么就一直阻塞等待第一个fn处理完成 拿到完成后的值。这个singleflight的作用期间也就是这个期间吧
g.mu.Lock()
delete(g.m,key) //更新 g.m
g.mu.Unlock()
请教博主: 能否在请求结束后 不删除g.m映射关系中的key?
我个人认为 需要删除的原因:
因为这个singleflight机制相当于是一个请求的缓冲器,不需要有储存功能。
@201732110125 你的理解是对的,缓存值的存储都放在 LRU 中,其他地方不保存数据。如果不删除,占用内存,且不会淘汰。
第一次见到这种提高并发的方式,多谢分享
不同客户端访问相同的key会受到影响吗?比如10个用户同一时间访问一个key,只有第一个能收到结果?
您好,问什么要使用waitgroup呢,实际上c.wg中同时最多只会有一个任务,使用group是不是太多了。
请问一下这样加锁,影响性能大不大呢
又被大佬鬼斧神刀般的技术震撼住了,此时的我内心久久不能平静
@hhyvs111 所有用户都能收到结果,请求是在服务端阻塞的,等待某一个查询返回结果的,其余请求直接复用这个结果了。singleflight 是这个目的。
@Jason210314 如果用信道,接受和发送需要一一对应, waitgroup 有 Add(1) 和 Done() 是一一对应的,但是可以有多个请求同时调用 Wait(),同时等待该任务结束, 一般锁和信道是做不到这一点的。
@HuntSweet 这样做的目的是提升性能,加锁的时间与访问数据源相比,可以忽略,
@wilgx0,哈哈,可以慢慢地理解下,singleflight 这个机制在缓存领域使用是非常广泛的,geecache 里实现的是一个比较简单的版本。感兴趣可以再看一看标准库里面的实现 x/sync/singleflight 。
这变量命名看的脑壳痛 a,b,c,d,e,f,g
@lire277 这个是 golang 非常著名的开源项目 groupcache singleflight的实现,,有点不容易看懂,但是看懂之后,会觉得非常地精巧和优雅,另外 golang 变量命名不会像 java 那么冗长。
这块代码真的太精巧了,看完后大呼过瘾的感觉。wg.Wait()用来阻塞当前的gorotinue,等待第一个调用返回的思想太精巧了。但其实这个还是会有问题,就是分布式情况下可能还是会产生并发请求,也就是说如果我们的GeeCache部署在集群上,落到不同pod的请求还是会并发访问数据库。但总得来说可以应付绝大多数场景了。
@shiluoye 一般 groupcache 之上,还会再增加一层封装,作为 API 层,上面这一层还会做一定的负载均衡的措施。singleflight 用来应对相同的并发请求是非常有效的。这里实现的是简单版本的。官方也实现了一个版本 x/sync/singleflight,更强大,感兴趣可以看一看~
跟了大佬的web和cache深感震撼 也受益匪浅 感觉这两年的php就是在纯搬砖 多谢多谢
package singleflight
type result struct {
val interface{}
err error
}
type entry struct {
res result
ready chan struct{}
}
type request struct {
key string
fn func() (interface{}, error)
response chan result
}
type Group struct{ requests chan request }
func New() *Group {
g := &Group{make(chan request)}
go g.serve()
return g
}
func (g *Group) Close() { close(g.requests) }
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
// Create request for each key
req := request{key, fn, make(chan result)}
// Send to g.serve handle
g.requests <- req
// Wait for response
ret := <-req.response
return ret.val, ret.err
}
func (g *Group) serve() {
// Cache the results of each key
cache := make(map[string]*entry)
// handle each request
for r := range g.requests {
if e, ok := cache[r.key]; !ok {
e := &entry{
ready: make(chan struct{}),
}
cache[r.key] = e
go e.call(r)
} else {
go e.deliver(r.response)
}
//I didn't implement a good way to delete the cache
}
}
func (e *entry) call(req request) {
e.res.val, e.res.err = req.fn()
req.response <- e.res
close(e.ready)
}
func (e *entry) deliver(resp chan<- result) {
<-e.ready
resp <- e.res
}
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
// ...
g := &Group{
// ...
loader: singleflight.New(), // different
}
return g
}
@fy403 当有多个请求请求同一个key值时,如何将多个阻塞的请求同时唤醒?你的代码应该只能唤醒其中一个阻塞的请求吧?
@runmark @fy403 当有多个请求请求同一个key值时,如何将多个阻塞的请求同时唤醒?你的代码应该只能唤醒其中一个阻塞的请求吧?
deliver可以唤醒其他相同请求了,但是这个没把缓存的结果删除。
太妙了我的天,感谢大佬
有个问题,group没有等待协程,只是等待函数调用结束也可以吗?感谢
假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?
假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?
不会出现这种情况,Do函数第一行就是加锁,只有一个协程可以拿到锁
@ppd0705
假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?
不会出现这种情况,Do函数第一行就是加锁,只有一个协程可以拿到锁
哦,你说得对,我只看到上面文章里面去掉g.mu锁的代码,忘了原来还有个g.mu锁了
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
我理解这样正好把同类请求合并了, 后来者只需要等结果就好了,这样不反而提高性能了吗?
@wiyan func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
面试就问到这个问题~
@cqzzg
@wiyan func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
面试就问到这个问题~
这些项目可以写到简历里面吗?(还没实习过的菜鸟)
实在没项目就写了这个,面试官都觉得简单,没怎么问
在 2022-05-03 11:42:11,"ts-cao" @.***> 写道:
@cqzzg
@wiyan func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
面试就问到这个问题~
这些项目可以写到简历里面吗?(还没实习过的菜鸟)
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
想问一下,有个细节没理清楚。我们现在已知的是,当我们在访问服务端的数据的时候,这个过程的其他进程如果也想访问会被阻塞,然后都会用到正在访问服务端的数据的返回的结果。但是,我们最后更新的delete时候,我们是不是就是说在这个过程中,如果需要再一次访问服务端的数据,又要重新访问服务端?我知道call的返回值回存入LRU中,但是我们delete后,势必会进行一次新的判断?
@wiyan func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
这个 Do 只有在缓存没有命中的时候才会执行,并不会影响缓存的本身的性能,缓存没命中的时候必然要等待获取数据,要么等上游返回,要么等锁。
在这里改用读写锁会不会省去这一步呢 func (c *cache)Get(key string)(val ByteView,ok bool){ // 使用读锁 c.mx.RLock() defer c.mx.RUnlock() if v,ok:=c.lru.Get(key);ok{ val = v.(ByteView) return val,ok } return }
func (c *cache)Set(key string,v ByteView){ // 使用写锁 c.mx.Lock() defer c.mx.Unlock() if c.lru == nil{ c.lru = NewCache(c.cacheBytes,nil) } c.lru.Set(key,v) }
对 m 加锁是为了达到懒汉单例 +控制并发读 的效果?
singleflight在目前的逻辑下,出现缓存失效时,要达到完完全全的“只从DB查询一次数据”这个目的应该是还存在一些问题的。 比如如下场景: 假设两个请求查询同一个key,对应的两个协程执行情况如下,并且先由协程2获取到了锁的话(这里协程2已经加载过一次数据了),那么协程1也会调用 fn 再进行一次数据加载
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
g.mu.Lock() // <---- 协程1执行到这里
if g.m == nil {
g.m = make(map[string]*call)
}
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() // <---- 协程2执行到这里
delete(g.m, key)
g.mu.Unlock()
return c.val, c.err
}
不过作为使用singleflight的示例还是很有收获的,感谢大佬分享~~
@1mnobody singleflight在目前的逻辑下,出现缓存失效时,要达到完完全全的“只从DB查询一次数据”这个目的应该是还存在一些问题的。 比如如下场景: 假设两个请求查询同一个key,对应的两个协程执行情况如下,并且先由协程2获取到了锁的话(这里协程2已经加载过一次数据了),那么协程1也会调用 fn 再进行一次数据加载
func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() // <---- 协程1执行到这里 if g.m == nil { g.m = make(map[string]*call) } 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() // <---- 协程2执行到这里 delete(g.m, key) g.mu.Unlock() return c.val, c.err }
不过作为使用singleflight的示例还是很有收获的,感谢大佬分享~~
这个是正常情况吧,singlefilght的目的是保证了当多次同时对fn发起调用的时候(这个时候一个fn还没有执行完成),只实际上调用一次fn,其他请求都等待fn的执行结果。你说的情况协程2执行到的地方对于fn的调用已经结束了,实际上协程1和协程2不是同时调用
https://geektutu.com/post/geecache-day6.html
7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了缓存雪崩、缓存击穿与缓存穿透的概念,使用 singleflight 防止缓存击穿,实现与测试。