blog
blog copied to clipboard
动手写分布式缓存 - GeeCache第六天 防止缓存击穿 | 极客兔兔
https://geektutu.com/post/geecache-day6.html
7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了缓存雪崩、缓存击穿与缓存穿透的概念,使用 singleflight 防止缓存击穿,实现与测试。
我有点蒙了
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?
我个人认为 需要删除的原因:
- 不删除,如果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就是在纯搬砖 多谢多谢
一种基于channel的并发实现
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() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢
面试就问到这个问题~