使用Golang的singleflight防止缓存击穿的方法

在使用缓存时,容易发生缓存击穿。

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

singleflight

介绍

import "golang.org/x/sync/singleflight"

singleflight类的使用方法就新建一个singleflight.Group,使用其方法Do或者DoChan来包装方法,被包装的方法在对于同一个key,只会有一个协程执行,其他协程等待那个协程执行结束后,拿到同样的结果。

Group结构体

代表一类工作,同一个group中,同样的key同时只能被执行一次。

Do方法

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool)

key:同一个key,同时只有一个协程执行。

fn:被包装的函数。

v:返回值,即执行的结果。其他等待的协程都会拿到。

shared:表示是否有其他协程得到了这个结果v。

DoChan方法

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result

与Do方法一样,只是返回的是一个channel,执行结果会发送到channel中,其他等待的协程都可以从channel中拿到结果。

ref:https://godoc.org/golang.org/x/sync/singleflight

示例

使用Do方法来模拟,解决缓存击穿的问题

func main() {

  var singleSetCache singleflight.Group

  getAndSetCache:=func (requestID int,cacheKey string) (string, error) {

  log.Printf("request %v start to get and set cache...",requestID)

  value,_, _ :=singleSetCache.Do(cacheKey, func() (ret interface{}, err error) {//do的入参key,可以直接使用缓存的key,这样同一个缓存,只有一个协程会去读DB

    log.Printf("request %v is setting cache...",requestID)

     time.Sleep(3*time._Second_)

     log.Printf("request %v set cache success!",requestID)

    return "VALUE",nil

   })

  return value.(string),nil

  }

  cacheKey:="cacheKey"

  for i:=1;i<10;i++{//模拟多个协程同时请求

  go func(requestID int) {

     value,_:=getAndSetCache(requestID,cacheKey)

     log.Printf("request %v get value: %v",requestID,value)

   }(i)

  }

  time.Sleep(20*time._Second_)
}

输出:

2020/04/12 18:18:40 request 4 start  to  get  and  set cache...

2020/04/12 18:18:40 request 4 is setting cache...

2020/04/12 18:18:40 request 2 start  to  get  and  set cache...

2020/04/12 18:18:40 request 7 start  to  get  and  set cache...

2020/04/12 18:18:40 request 5 start  to  get  and  set cache...

2020/04/12 18:18:40 request 1 start  to  get  and  set cache...

2020/04/12 18:18:40 request 6 start  to  get  and  set cache...

2020/04/12 18:18:40 request 3 start  to  get  and  set cache...

2020/04/12 18:18:40 request 8 start  to  get  and  set cache...

2020/04/12 18:18:40 request 9 start  to  get  and  set cache...

2020/04/12 18:18:43 request 4 set  cache  success!

2020/04/12 18:18:43 request 4 get value: VALUE

2020/04/12 18:18:43 request 9 get value: VALUE

2020/04/12 18:18:43 request 6 get value: VALUE

2020/04/12 18:18:43 request 3 get value: VALUE

2020/04/12 18:18:43 request 8 get value: VALUE

2020/04/12 18:18:43 request 1 get value: VALUE

2020/04/12 18:18:43 request 5 get value: VALUE

2020/04/12 18:18:43 request 2 get value: VALUE

2020/04/12 18:18:43 request 7 get value: VALUE`

可以看到确实只有一个协程执行了被包装的函数,并且其他协程都拿到了结果。

源码分析

看一下这个Do方法是怎么实现的。

首先看一下Group的结构:

type Group struct {

  mu sync.Mutex   

  m map[string]*call //保存key对应的函数执行过程和结果的变量。

}

Group的结构非常简单,一个锁来保证并发安全,另一个map用来保存key对应的函数执行过程和结果的变量。

看下call的结构:

type call struct {

  wg sync.WaitGroup //用WaitGroup实现只有一个协程执行函数

  val interface{} //函数执行结果

  err error

  forgotten bool

  dups int //含义是duplications,即同时执行同一个key的协程数量

  chans []chan<- Result
}

看下Do方法

func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {

  g.mu.Lock()//写Group的m字段时,加锁保证写安全。

  if g.m == nil {

  g.m = make(map[string]*call)

  }

if c, ok := g.m[key]; ok {//如果key已经存在,说明已经有协程在执行,则dups++,并等待其执行完毕后,返回其执行结果,执行结果保存在对应的call的val字段里

   c.dups++

   g.mu.Unlock()

   c.wg.Wait()

 return c.val, c.err, true

  }

//如果key不存在,则新建一个call,并使用WaitGroup来阻塞其他协程,同时在m字段里写入key和对应的call

c := new(call)

  c.wg.Add(1)

  g.m[key] = c

  g.mu.Unlock()

  g.doCall(c, key, fn)//第一个进来的协程来执行这个函数

return c.val, c.err, c.dups > 0

}

继续看下g.doCall里具体干了什么

func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {

  c.val, c.err = fn()//执行被包装的函数

  c.wg.Done()//执行完毕后,就可以通知其他协程可以拿结果了

  g.mu.Lock()

if !c.forgotten {//其实这里是为了保证执行完毕之后,对应的key被删除,Group有一个方法Forget(key string),可以用来主动删除key,这里是判断那个方法是否被调用过,被调用过则字段forgotten会置为true,如果没有被调用过,则在这里把key删除。

  delete(g.m, key)

  }

  for _, ch := range c.chans {//将执行结果发送到channel里,这里是给DoChan方法使用的

  ch <- Result{c.val, c.err, c.dups > 0}

  }

  g.mu.Unlock()

}

由此看来,其实现是非常简单的。不得不赞叹一百来行代码就实现了功能。

其他

顺便附上DoChan方法的使用示例:

func main() {

  var singleSetCache singleflight.Group

  getAndSetCache:=func (requestID int,cacheKey string) (string, error) {

  log.Printf("request %v start to get and set cache...",requestID)

  retChan:=singleSetCache.DoChan(cacheKey, func() (ret interface{}, err error) {

    log.Printf("request %v is setting cache...",requestID)

    time.Sleep(3*time._Second_)

    log.Printf("request %v set cache success!",requestID)

    return "VALUE",nil

   })

  var ret singleflight.Result

  timeout := time.After(5 * time._Second_)

  select {//加入了超时机制

    case <-timeout:

      log.Printf("time out!")

      return "",errors.New("time out")

    case ret =<- retChan://从chan中取出结果

      return ret.Val.(string),ret.Err

   }

  return "",nil

  }

  cacheKey:="cacheKey"

  for i:=1;i<10;i++{

  go func(requestID int) {

     value,_:=getAndSetCache(requestID,cacheKey)

     log.Printf("request %v get value: %v",requestID,value)

   }(i)

  }

  time.Sleep(20*time._Second_)

}

看下DoChan的源码

func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {

  ch := make(chan Result, 1)

  g.mu.Lock()

  if g.m == nil {

  g.m = make(map[string]*call)

  }

  if c, ok := g.m[key]; ok {

   c.dups++

c.chans = append(c.chans, ch)//可以看到,每个等待的协程,都有一个结果channel。从之前的g.doCall里也可以看到,每个channel都给塞了结果。为什么不所有协程共用一个channel?因为那样就得在channel里塞至少与协程数量一样的结果数量,但是你却无法保证用户一个协程只读取一次。

   g.mu.Unlock()

   return ch

  }

  c := &call{chans: []chan<- Result{ch}}

  c.wg.Add(1)

  g.m[key] = c

  g.mu.Unlock()

  go g.doCall(c, key, fn)

  return ch
}

到此这篇关于使用Golang的singleflight防止缓存击穿的方法的文章就介绍到这了,更多相关Golang singleflight防止缓存击穿内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang实现LRU缓存淘汰算法的示例代码

    LRU缓存淘汰算法 LRU是最近最少使用策略的缩写,是根据数据的历史访问记录来进行淘汰数据,其核心思想是"如果数据最近被访问过,那么将来被访问的几率也更高". 双向链表实现LRU 将Cache的所有位置都用双链表连接起来,当一个位置被访问(get/put)之后,通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中. 这样,在多次操作后,最近被访问(get/put)的,就会被向链表头方向移动,而没有访问的,向链表后方移动,链表尾则表示最近最少使用的Cache

  • 使用Golang的singleflight防止缓存击穿的方法

    在使用缓存时,容易发生缓存击穿. 缓存击穿:一个存在的key,在缓存过期的瞬间,同时有大量的请求过来,造成所有请求都去读dB,这些请求都会击穿到DB,造成瞬时DB请求量大.压力骤增. singleflight 介绍 import "golang.org/x/sync/singleflight" singleflight类的使用方法就新建一个singleflight.Group,使用其方法Do或者DoChan来包装方法,被包装的方法在对于同一个key,只会有一个协程执行,其他协程等待那个

  • golang 防缓存击穿singleflight的实现

    目录 一.什么是缓存击穿 二.原理 三.实现 一.什么是缓存击穿 当一个key是热点key时,一般会做缓存来抗大量并发,但当缓存失效的一瞬间,这些大量的并发请求会击穿缓存,直接请求数据库 为了避免缓存击穿,一种解决方法可以设置缓存永不过期,另一种可以使用golang的包 singleflight golang.org/x/sync/singleflight 二.原理 多个并发请求对一个失效key进行数据获取时,只会有其中一个去直接获取数据,其它请求会阻塞等待第一个请求返回给它们结果 三.实现 p

  • c# 如何用lock解决缓存击穿

    背景 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力. 解决方案 1.设置热点数据永远不过期. 2.加互斥锁,互斥锁参考代码如下: 2.1.根据key生成object() private static object GetMemoryCacheLockObject(string key) { string cacheLockKey = string.Format(Memor

  • JAVA面试题之缓存击穿、缓存穿透、缓存雪崩的三者区别

    目录 调用链路 缓存击穿 含义: 解决方案: 缓存穿透 含义: 解决方案: 缓存雪崩 含义: 解决方案: 前端发起一个请求,经历过三次握手后连接到服务器,想要获取相应的数据,那么服务器接入了缓存中间件后,从接收到Request到最后的Response,到底是怎样的一个流程呢?以下探讨忽略掉参数校验等逻辑,直接讲最核心的链路. 调用链路 一个请求Request过来,服务器首先和缓存中间件建立连接,传输对应key到缓存中间件中获取相对应的数据,服务器拿到返回的结果后,判断返回的结果是否有数据,如果有

  • Redis分布式锁防止缓存击穿的实现

    缓存击穿 和缓存穿透不同的是,缓存击穿是指:缓存中没有,但是数据库中存在的热点数据. 例如:首页的热点新闻,并发访问量非常大的热点数据,如果缓存过期失效,服务器会去查询DB,这时候如果大量的并发去查询DB,可能会瞬间压垮DB. 画了个简图,如下所示: 解决方案:DB查询加分布式锁. 未加锁的情况 解决问题之前,先看一下不做处理的代码和运行情况. 根据商品ID查询商品详情代码 清空Redis缓存,开启5个线程去并发访问测试,测试代码如下: 我们预期希望DB只查询一次,后面4个查询从Redis缓存中

  • 详解Java redis中缓存穿透 缓存击穿 雪崩三种现象以及解决方法

    目录 前言 一.缓存穿透 二.缓存击穿 三.雪崩现象 总结 前言 本文主要阐述redis中的三种现象 1.缓存穿透 2.缓存击穿 3.雪崩现象 本文主要说明本人对三种情况的理解,如果需要知道redis基础请查看其他博客,加油! 一.缓存穿透 理解:何为缓存穿透,先要了解穿透,这样有助于区分穿透和击穿,穿透就类似于伤害一点一点的累计,最终打到穿透的目的,类似于射手,一下一下普通攻击,最终杀死对方,先上图 先来描述一下缓存穿透的过程: 1.由于我们取数据的原则是先查询redis上,如果redis上有

  • 详解Spring Cache使用Redisson分布式锁解决缓存击穿问题

    目录 1 什么是缓存击穿 2 为什么要使用分布式锁 3 什么是Redisson 4 Spring Boot集成Redisson 4.1 添加maven依赖 4.2 配置yml 4.3 配置RedissonConfig 5 使用Redisson的分布式锁解决缓存击穿 1 什么是缓存击穿 一份热点数据,它的访问量非常大.在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃. 2 为什么要使用分布式锁 在项目中,当共享资源出现竞争情况的时候,为了防止出现并发问题,我们一般会采用锁机制来控制.在单机环境

  • 使用spring-cache一行代码解决缓存击穿问题

    目录 引言 正文 目前缺陷 真正方案 缓存穿透 缓存击穿 缓存雪崩 文末 引言 今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器啊,嗯,你们懂的),其实这类方案并不适合在项目中直接落地. 那么,我们在项目中落地代码的时候,其实只需要一个注解就能解决这些问题,并不需要搞的那么复杂. 本文有一个前提,读者必须是java栈,且是用Springboot构建自己的项目,如果是go技术栈或者python技术栈的,可能介绍的思路仅供大家参

  • 浅谈Redis缓存击穿、缓存穿透、缓存雪崩的解决方案

    目录 前言 Redis缓存使用场景 Redis缓存穿透 解决方案 1.对空值缓存 2.添加参数校验 3.采用布隆过滤器 Redis缓存雪崩 解决方案 1.大量热点数据同时失效带来的缓存雪崩问题 2. 服务降级 3. Redis 缓存实例发生故障宕机带来的缓存雪崩问题 Redis缓存击穿 解决方案 1. 热key不过期 2. 分布式锁 总结 缓存击穿 缓存穿透 缓存雪崩 前言 在日常的项目中,缓存的使用场景是比较多的.缓存是分布式系统中的重要组件,主要解决在高并发.大数据场景下,热点数据访问的性能

  • golang实现微信支付v3版本的方法

    一.准备阶段 获取私钥 官方文档 https://kf.qq.com/faq/161222N... 获取私钥证书的序列号 https://pay.weixin.qq.com/wik... openssl x509 -in 1900009191_20180326_cert.pem -noout -serial serial=1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C 私钥获取后有三个文件 apiclient_key.p12 apiclient_cert.pem

随机推荐