GEE 组件篇 -- singleflight

文章详细分析了GEE组件中的singleflight实现,该组件用于优化相同key的重复取数操作,通过并发控制确保只执行一次。文章涵盖了代码结构、测试及与其他工具的比较,特别强调了其在并发环境下的效率和对缓存穿透的防御作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

GEE 组件篇 – singleflight

详细可查看https://github.com/OddSpilit/gee/blob/master/gee/geecache/singleflight/singleflight.go 源码

  • singleflight我相信大家应该都挺熟悉的,主要的功能就是对相同key重复的取数操作简化为一次操作。这次的流程主要有以下几点:

代码分析

  • 两个结构体:call && Group, 一个方法Do
    在这里插入图片描述

  • call结构体主要是用来保存数据的值,以及通过sync.WaitGroup包来控制并发解锁后让重复获取相同key的请求处于等待状态。

  • Group:锁负责控制并发,m是一个map类型,负责存储键值对。

    • Do方法:我们贴上代码解析一下这块流程:

      /**
      每次大量请求, 控制fn函数执行次数
      */
      func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
      	g.mu.Lock() // 1. 请求进来即锁住
      	if g.m[key] == nil {
      		g.m = make(map[string]*call)
      	}
      
        // 2. 如果是第二次以及之后的请求进来,就直接通过Wait操作等待Done操作信息(获取相同key)
      	if c, ok := g.m[key]; ok {
      		g.mu.Unlock()
      		c.wg.Wait()
      		return c.val, c.err
      	}
      
        // 3. 首次进来逻辑:
      	c := new(call)
      	c.wg.Add(1)
      	g.m[key] = c
      	g.mu.Unlock() // 已经设置了key => c, 没必要锁, 让继续进来的请求去到步骤2
      	c.val, c.err = fn() // 执行方法, 获取数据
      	c.wg.Done() // 告知已完成值跟错误信息的设置
      
      	// 貌似这块可以不加锁操作
      	g.mu.Lock()
      	delete(g.m, key) // 删除,保证每次第一次进来都不会走到步骤2
      	g.mu.Unlock()
      
      	return c.val, c.err
      }
      

在这里插入图片描述

代码测试

在这里插入图片描述

现有singleflight工具比较

  • 结构体对比:

    • call结构体增加了dups int成员,因为调用Do方法的时候会返回是否是数据共享(即是否重复获取了一个key值),如果dups > 0 即为true,还有chans []chan<- Result 保存异步数据结果集
    • 新增了panicError结构体,自定义错误信息
    • 新增了Result结构体,主要是服务于DoChan方法,通过返回只读通道支持异步操作过程,里面的操作跟Do差不多,只是把fn结果集通过chan收集起来了
  • 方法对比:

    • Do

      func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
      	g.mu.Lock()
      	if g.m == nil {
      		g.m = make(map[string]*call)
      	}
        // 主要是这里有点区别
      	if c, ok := g.m[key]; ok {
      		c.dups++
      		g.mu.Unlock()
      		c.wg.Wait()
      		// 判断是否出现异常错误或者自定义的错误,这些错误会在`doCall`方法体现出来
      		if e, ok := c.err.(*panicError); ok {
      			panic(e)
      		} else if c.err == errGoexit {
      			runtime.Goexit()
      		}
      		return c.val, c.err, true
      	}
      	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
      }
      
    • DoChan: 这一块代码比较简单,读者可以自己分析一下,重点讲一下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)
    		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
    }
    
    
    • doCall: 执行了两个defer操作

      func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
        // 通过两个 defer 巧妙的区分了到底是发生了 panic 还是用户主动调用了 runtime.Goexit
      	normalReturn := false
      	recovered := false
        // 最后执行的逻辑,先看下面的操作
      	defer func() {
          // 正常时这里的normalReturn跟recovered为true
      		if !normalReturn && !recovered {
      			c.err = errGoexit
      		}
      		g.mu.Lock()
      		defer g.mu.Unlock()
      		c.wg.Done()
      		if g.m[key] == c {
      			delete(g.m, key)
      		}
          // fn 执行出错
      		if e, ok := c.err.(*panicError); ok {
            // Dochan操作,因为DoChan是异步操作,用select{}做阻塞操作,其实这里就是会让panic信息输出出来,不会因为recover操作而不输出
      			if len(c.chans) > 0 {
      				go panic(e)
      				select {} 
      			} else {
      				panic(e)
      			}
      		} else if c.err == errGoexit { // 执行了runtime.Goexit退出
      		} else { // 收集数据
      			for _, ch := range c.chans {
      				ch <- Result{c.val, c.err, c.dups > 0}
      			}
      		}
      	}()
      	
      	func() {
      		defer func() {
            // 如果fn方法异常,实例化panic
      			if !normalReturn {
      				if r := recover(); r != nil {
      					c.err = newPanicError(r)
      				}
      			}
      		}()
      		c.val, c.err = fn() // 获取数据来到了这里
          // 定义为正常退出
      		normalReturn = true
      	}()
        // 如果不是正常退出,进来了就是recover错误类型
      	if !normalReturn {
      		recovered = true
      	}
      }
      
    • Forget:手动移除某个key的数据

      func (g *Group) Forget(key string) {
      	g.mu.Lock()
      	delete(g.m, key)
      	g.mu.Unlock()
      }
      

总结

  • 之前在写这块代码的时候就觉得这个操作很轻巧,作为缓存,他不会说需要等待结果收集完成之后再把运行权交出去,而是会让重复的请求先做一个等待操作,并且第一次获取到的结果又是可以共享的。
  • singleflight是一个阅读难度不大,但是能学到很多东西的一个防缓存穿透利器。
  • 其实我是先打算讲lru但是我觉得singleflight的难度相比lru跟一致性hash还是简单的,也先让读者对缓存这块内容有个简单的了解。
  • 后面还是着重讲缓存这一方面的内容为后面重头戏geecache做一个铺垫。👻👻
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值