blog icon indicating copy to clipboard operation
blog copied to clipboard

动手写分布式缓存 - GeeCache第六天 防止缓存击穿 | 极客兔兔

Open geektutu opened this issue 5 years ago • 38 comments

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

7天用 Go语言/golang 从零实现分布式缓存 GeeCache 教程(7 days implement golang distributed cache from scratch tutorial),动手写分布式缓存,参照 groupcache 的实现。本文介绍了缓存雪崩、缓存击穿与缓存穿透的概念,使用 singleflight 防止缓存击穿,实现与测试。

geektutu avatar Feb 16 '20 17:02 geektutu

我有点蒙了

xiaoxfan avatar Mar 12 '20 13:03 xiaoxfan

singleflight这样理解对不对: 在一瞬间有大量请求get(key),而且key未被缓存或者未被缓存在当前节点 如果不用singleflight,那么这些请求都会发送远端节点或者从本地数据库读取,会造成远端节点或本地数据库压力猛增。使用singleflight,第一个get(key)请求到来时,singleflight会记录当前key正在被处理,后续的请求只需要等待第一个请求处理完成,取返回值即可。

xiaoxfan avatar Mar 17 '20 01:03 xiaoxfan

@xiaoxfan 你的理解是正确的,并发场景下如果 GeeCache 已经向其他节点/源获取数据了,那么就加锁阻塞其他相同的请求,等待请求结果,防止其他节点/源压力猛增被击穿。

geektutu avatar Mar 17 '20 02:03 geektutu

当有一个key请求发起后 fn处理函数处理过程中 如果有其他相同key的请求过来 那么就一直阻塞等待第一个fn处理完成 拿到完成后的值。这个singleflight的作用期间也就是这个期间吧

walkmiao avatar Mar 27 '20 02:03 walkmiao

g.mu.Lock()
delete(g.m,key) //更新 g.m
g.mu.Unlock()

请教博主: 能否在请求结束后 不删除g.m映射关系中的key?


我个人认为 需要删除的原因:

  1. 不删除,如果key对应的值变化,所得的值还是旧值
  2. 占用内存

因为这个singleflight机制相当于是一个请求的缓冲器,不需要有储存功能。

  • 在少量访问时,正常使用。
  • 在大量并发访问时,对于并发的信息,共享第一个请求的返回值,大幅减少请求次数

201732110125 avatar Apr 14 '20 07:04 201732110125

@201732110125 你的理解是对的,缓存值的存储都放在 LRU 中,其他地方不保存数据。如果不删除,占用内存,且不会淘汰。

geektutu avatar Apr 14 '20 09:04 geektutu

第一次见到这种提高并发的方式,多谢分享

ppd0705 avatar May 13 '20 13:05 ppd0705

不同客户端访问相同的key会受到影响吗?比如10个用户同一时间访问一个key,只有第一个能收到结果?

hhyvs111 avatar May 21 '20 03:05 hhyvs111

您好,问什么要使用waitgroup呢,实际上c.wg中同时最多只会有一个任务,使用group是不是太多了。

waruto210 avatar Jul 31 '20 23:07 waruto210

请问一下这样加锁,影响性能大不大呢

HuntSweet avatar Aug 16 '20 09:08 HuntSweet

又被大佬鬼斧神刀般的技术震撼住了,此时的我内心久久不能平静

wilgx0 avatar Oct 13 '20 08:10 wilgx0

@hhyvs111 所有用户都能收到结果,请求是在服务端阻塞的,等待某一个查询返回结果的,其余请求直接复用这个结果了。singleflight 是这个目的。

@Jason210314 如果用信道,接受和发送需要一一对应, waitgroup 有 Add(1) 和 Done() 是一一对应的,但是可以有多个请求同时调用 Wait(),同时等待该任务结束, 一般锁和信道是做不到这一点的。

@HuntSweet 这样做的目的是提升性能,加锁的时间与访问数据源相比,可以忽略,

geektutu avatar Oct 13 '20 15:10 geektutu

@wilgx0,哈哈,可以慢慢地理解下,singleflight 这个机制在缓存领域使用是非常广泛的,geecache 里实现的是一个比较简单的版本。感兴趣可以再看一看标准库里面的实现 x/sync/singleflight

geektutu avatar Oct 13 '20 15:10 geektutu

这变量命名看的脑壳痛 a,b,c,d,e,f,g

a-long-way avatar Nov 06 '20 03:11 a-long-way

@lire277 这个是 golang 非常著名的开源项目 groupcache singleflight的实现,,有点不容易看懂,但是看懂之后,会觉得非常地精巧和优雅,另外 golang 变量命名不会像 java 那么冗长。

geektutu avatar Nov 06 '20 05:11 geektutu

这块代码真的太精巧了,看完后大呼过瘾的感觉。wg.Wait()用来阻塞当前的gorotinue,等待第一个调用返回的思想太精巧了。但其实这个还是会有问题,就是分布式情况下可能还是会产生并发请求,也就是说如果我们的GeeCache部署在集群上,落到不同pod的请求还是会并发访问数据库。但总得来说可以应付绝大多数场景了。

shiluoye avatar Dec 15 '20 13:12 shiluoye

@shiluoye 一般 groupcache 之上,还会再增加一层封装,作为 API 层,上面这一层还会做一定的负载均衡的措施。singleflight 用来应对相同的并发请求是非常有效的。这里实现的是简单版本的。官方也实现了一个版本 x/sync/singleflight,更强大,感兴趣可以看一看~

geektutu avatar Dec 16 '20 01:12 geektutu

跟了大佬的web和cache深感震撼 也受益匪浅 感觉这两年的php就是在纯搬砖 多谢多谢

651619114 avatar Mar 16 '21 02:03 651619114

一种基于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 avatar Apr 05 '21 14:04 fy403

@fy403 当有多个请求请求同一个key值时,如何将多个阻塞的请求同时唤醒?你的代码应该只能唤醒其中一个阻塞的请求吧?

runmark avatar Jun 21 '21 09:06 runmark

@runmark @fy403 当有多个请求请求同一个key值时,如何将多个阻塞的请求同时唤醒?你的代码应该只能唤醒其中一个阻塞的请求吧?

deliver可以唤醒其他相同请求了,但是这个没把缓存的结果删除。

xiaoheng14 avatar Aug 13 '21 09:08 xiaoheng14

太妙了我的天,感谢大佬

iiiuwioajdks avatar Sep 21 '21 08:09 iiiuwioajdks

有个问题,group没有等待协程,只是等待函数调用结束也可以吗?感谢

zmf173417897 avatar Dec 06 '21 03:12 zmf173417897

  • 博主太强了

018429 avatar Dec 06 '21 12:12 018429

假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?

ningzero avatar Feb 05 '22 09:02 ningzero

假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?

不会出现这种情况,Do函数第一行就是加锁,只有一个协程可以拿到锁

ppd0705 avatar Feb 05 '22 11:02 ppd0705

@ppd0705

假设有a,b两个协程是请求同一个key的,a走到了c.wg.Add(1),还没来得及g.m[key] = c,而b也走到了c.wg.Add(1),那还是会发起两次相同请求吧,这种情况能避免么?

不会出现这种情况,Do函数第一行就是加锁,只有一个协程可以拿到锁

哦,你说得对,我只看到上面文章里面去掉g.mu锁的代码,忘了原来还有个g.mu锁了

ningzero avatar Feb 06 '22 08:02 ningzero

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢

wiyan avatar Mar 13 '22 04:03 wiyan

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢

我理解这样正好把同类请求合并了, 后来者只需要等结果就好了,这样不反而提高性能了吗?

ppd0705 avatar Mar 13 '22 13:03 ppd0705

@wiyan func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) { g.mu.Lock() 请问这里就是无论谁进来,都会在这等来同一个锁,这样高并发下是否会影响性能呢

面试就问到这个问题~

BetterZhuang avatar Apr 13 '22 12:04 BetterZhuang