• Go分布式缓存 防止缓存击穿(day6)


    Go分布式缓存 防止缓存击穿(day6)

    • 缓存雪崩、缓存击穿与缓存穿透的概念简介。
    • 使用 singleflight 防止缓存击穿,实现与测试。代码约70行

    1 缓存雪崩、缓存击穿与缓存穿透

    GeeCache 第五天 提到了缓存雪崩和缓存击穿,在这里做下总结:

    缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。

    缓存击穿:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。

    缓存穿透:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

    2 singleflight 的实现

    还记得 GeeCache 第五天 最后的测试结果吗?

    2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001
    2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001
    2020/02/16 21:17:45 [Server http://localhost:8003] Pick peer http://localhost:8001
    
    • 1
    • 2
    • 3

    我们并发了 N 个请求 ?key=Tom,8003 节点向 8001 同时发起了 N 次请求。假设对数据库的访问没有做任何限制的,很可能向数据库也发起 N 次请求,容易导致缓存击穿和穿透。即使对数据库做了防护,HTTP 请求是非常耗费资源的操作,针对相同的 key,8003 节点向 8001 发起三次请求也是没有必要的。那这种情况下,我们如何做到只向远端节点发起一次请求呢?

    geecache 实现了一个名为 singleflight 的 package 来解决这个问题。

    day6-single-flight/geecache/singleflight/singleflight.go - github

    首先创建 callGroup 类型。

    package singleflight
    
    import "sync"
    
    type call struct {
    	wg  sync.WaitGroup
    	val interface{}
    	err error
    }
    
    type Group struct {
    	mu sync.Mutex       // protects m
    	m  map[string]*call
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • call 代表正在进行中,或已经结束的请求。使用 sync.WaitGroup 锁避免重入。
    • Group 是 singleflight 的主数据结构,管理不同 key 的请求(call)。

    实现 Do 方法

    func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    	g.mu.Lock()
    	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()
    	delete(g.m, key)
    	g.mu.Unlock()
    	
    	return c.val, c.err
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • Do 方法,接收 2 个参数,第一个参数是 key,第二个参数是一个函数 fn。Do 的作用就是,针对相同的 key,无论 Do 被调用多少次,函数 fn 都只会被调用一次,等待 fn 调用结束了,返回返回值或错误。

    g.mu 是保护 Group 的成员变量 m 不被并发读写而加上的锁。为了便于理解 Do 函数,我们将 g.mu 暂时去掉。并且把 g.m 延迟初始化的部分去掉,延迟初始化的目的很简单,提高内存使用效率。

    剩下的逻辑就很清晰了:

    func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    	if c, ok := g.m[key]; ok {
    		c.wg.Wait()   // 如果请求正在进行中,则等待
    		return c.val, c.err  // 请求结束,返回结果
    	}
    	c := new(call)
    	c.wg.Add(1)       // 发起请求前加锁
    	g.m[key] = c      // 添加到 g.m,表明 key 已经有对应的请求在处理
    
    	c.val, c.err = fn() // 调用 fn,发起请求
    	c.wg.Done()         // 请求结束
    
        delete(g.m, key)    // 更新 g.m
        
    	return c.val, c.err // 返回结果
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    并发协程之间不需要消息传递,非常适合 sync.WaitGroup

    • wg.Add(1) 锁加1。
    • wg.Wait() 阻塞,直到锁被释放。
    • wg.Done() 锁减1。

    3 singleflight 的使用

    day6-single-flight/geecache/geecache.go - github

    type Group struct {
    	name      string
    	getter    Getter
    	mainCache cache
    	peers     PeerPicker
    	// use singleflight.Group to make sure that
    	// each key is only fetched once
    	loader *singleflight.Group
    }
    
    func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
        // ...
    	g := &Group{
            // ...
    		loader:    &singleflight.Group{},
    	}
    	return g
    }
    
    func (g *Group) load(key string) (value ByteView, err error) {
    	// each key is only fetched once (either locally or remotely)
    	// regardless of the number of concurrent callers.
    	viewi, err := g.loader.Do(key, func() (interface{}, error) {
    		if g.peers != nil {
    			if peer, ok := g.peers.PickPeer(key); ok {
    				if value, err = g.getFromPeer(peer, key); err == nil {
    					return value, nil
    				}
    				log.Println("[GeeCache] Failed to get from peer", err)
    			}
    		}
    
    		return g.getLocally(key)
    	})
    
    	if err == nil {
    		return viewi.(ByteView), nil
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 修改 geecache.go 中的 Group,添加成员变量 loader,并更新构建函数 NewGroup
    • 修改 load 函数,将原来的 load 的逻辑,使用 g.loader.Do 包裹起来即可,这样确保了并发场景下针对相同的 key,load 过程只会调用一次。

    4 总结

    使用waitGroup 锁机制 实现请求的并发控制,保证只执行一次函数。

  • 相关阅读:
    19.数据结构和算法的交叉口,下一章进入算法介绍
    基于nodejs的电影交流网站
    Web前端—CSS高级(定位、高级技巧、CSS修饰属性、综合案例:购物网站轮播图)
    汉朔科技IPO:引领智慧零售新时代,推动行业数字化转型
    FormData props
    内网离线安装Nginx并配置SSL
    TypeScript(6)函数
    【读书笔记】《寻路中国-从乡村到工厂的自驾之旅》
    python类中的下划线
    Qt通过ODBC连接openGauss数据库
  • 原文地址:https://blog.csdn.net/weixin_45750972/article/details/127915993