前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang源码分析:singleflight

golang源码分析:singleflight

作者头像
golangLeetcode
发布2022-08-02 19:23:10
5750
发布2022-08-02 19:23:10
举报

singleflight通常被用来做防止缓存击穿,代码位置在https://github.com/golang/groupcache/tree/master/singleflight,在详细介绍代码内容之前,我先区分下雪崩、穿透和击穿:

雪崩

雪崩就是指缓存中大批量热点数据同时过期或缓存机器意外发生了全盘宕机后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。

解决办法:

将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。

简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。

事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

缓存穿透

缓存穿透是指段时间涌入大量请求,缓存中查不到,每次你去数据库里查,也查不到。(数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。)这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

缓存击穿

缓存击穿,某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

方法一:我们简单粗暴点,直接让热点数据永远不过期,定时任务定期去刷新数据就可以了。不过这样设置需要区分场景,比如某宝首页可以这么做。

方法二:为了避免出现缓存击穿的情况,我们可以在第一个请求去查询数据库的时候对他加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,后面的线程进来发现已经有缓存了,就直接走缓存,从而保护数据库。但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降。需要结合实际的业务去考虑是否要这么做。

方法三:就是singleflight的设计思路,也会使用互斥锁,但是相对于方法二的加锁粒度会更细

singleflight 源码分析

说完了singleflight的应用场景,下面详细分析下singleflight的源码,源码非常简洁,目录下就包含了两个文件singleflight.go 和对应的测试的测试文件singleflight_test.go

源码中就定义了两个结构体和一个方法

代码语言:javascript
复制
// call is an in-flight or completed Do call
type call struct {
  wg  sync.WaitGroup
  val interface{}
  err error
}
代码语言:javascript
复制
// Group represents a class of work and forms a namespace in which
// units of work can be executed with duplicate suppression.
type Group struct {
  mu sync.Mutex       // protects m
  m  map[string]*call // lazily initialized
}

通过call的waitGroup来阻塞相同key的请求,实现了一个指允许一个请求到后端,通过Group的m来实现相同key的数据共享,大家取同一份结果,下面看下Do函数的具体实现:

代码语言:javascript
复制
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
//同一个对象多次同时多次调用这个逻辑的时候,可以使用其中的一个去执行
func (g *Group) Do(key string, fn func()(interface{},error)) (interface{}, error ){
    g.mu.Lock() //加锁保护存放key的map,因为要并发执行
    if g.m == nil { //lazing make 方式建立
        g.m = make(map[string]*call)
    }
    if c, ok := g.m[key]; ok { //如果map中已经存在对这个key的处理那就等着吧
        g.mu.Unlock() //解锁,对map的操作已经完毕
        c.wg.Wait()
        return c.val,c.err //map中只有一份key,所以只有一个c
    }
    c := new(call) //创建一个工作单元,只负责处理一种key
    c.wg.Add(1)
    g.m[key] = c //将key注册到map中
    g.mu.Unlock() //map的操做完成,解锁
    
    c.val, c.err = fn()//第一个注册者去执行
    c.wg.Done()
    
    g.mu.Lock()
    delete(g.m,key) //对map进行操作,需要枷锁
    g.mu.Unlock()
    
    return c.val, c.err //给第一个注册者返回结果
}

执行过程如下:

1,对于相同key的请求,大家抢锁,只有第一个请求可以获得锁;

2,然后查询map发现没有数据,创建一个call,waitGroup加1,写入map,然后释放锁;做到了锁的粒度最小化。

3,其他获得锁的请求,从map中取到call,由于函数fn还没有执行完毕,所以waitGroup还在等待状态,后面获得锁的请求都在等待这个waitGroup;

4,当函数执行完毕以后,获得了数据,调用wg.Done()通知所有等待的请求获取数据,实现了大家共享一份数据;

5,然后加锁做清理工作,清理掉map里存储的数据。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-08-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档