duplication suppression --- 让你的缓存更聪明

在引入缓存组件的业务体系中,下面代码是很常见的:

v = cache.get('key')
if v is None:
	v = fetch_from_db('key')
	cache.set('key', v)
# 本例与语言无关

尽管上面代码在单线程代码中工作的很好,但是在多线程、或者多协程服务中则还存在一定的优化空间。
考虑如下请求序列:

# without duplication v
|---------200ms-----------|
|---------req1----------->|
	|---------req2----------->|
					   |---------req3----------->|

# with duplication suppression
|---------200ms-----------|
|---------req1----------->|
	|---------req2------->|
					 |req3|
-------------------------------------------------->time

在没有 duplication suppression 的情况下,req1-3 都去检查 cache 内 foo 是否存在,并且都会调用 fetch_from_db() 。但是这个本质上只需要进行一次,重复的操作不但会对 db 造成性能负担,还会导致并发请求的响应变慢。
在有 duplication suppression 的情况下,req2、req3 则不需要自己去重复的请求 db,都可以在 req1 结束时完成。
duplication suppression 则是在一个进程内,通过抑制重复的耗时操作,来提升性能的优化方式。
golang/groupcache 就利用了这个技术,对 cache 服务做了优化

groupcache

groupcache 是一个优秀而简单的分布式缓存框架,不同于 redis 或者 memcached,其自带 cache-filling 功能。
不同于 redis、memcached 这些缓存组件,groupcache 不要求必须运行一套单独的缓存服务集群。其既是一个 client 也是一个 server。这样子降低了服务部署复杂度。不过很多时候,缓存服务和业务服务进行进程隔离,可能也更有利于服务稳定性,个中取舍要结合业务来决定。
下面我们来分析一下 groupcache 的源码,探究下其是如何实现的。

singleflight

singleflight 是实现 duplication suppression 的核心。其实现也并不复杂:

package singleflight

import "sync"

// call is an in-flight or completed Do call
type call struct {
	wg  sync.WaitGroup
	val interface{}
	err error
}

// 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
}

// 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()
	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
	}
	// 这是第一次调用,构造 call 对象,更新 map,并且释放锁
	// 注意在进行真正调用前要释放锁,缩短临界区
	c := new(call)
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()
	// 执行函数调用
	c.val, c.err = fn()
	c.wg.Done()
	// 删除 entry,要不然后续的同一个 key 调用会直接返回之前的结果
	g.mu.Lock()
	delete(g.m, key)
	g.mu.Unlock()

	return c.val, c.err
}

singleflight 可以让针对同一个 key 其并发的函数调用只执行一次。
可以想到,利用 singleflight 我们可以让并发去读取同一个 key 的请求,只对底层存储读取一次即可。大家感兴趣的话,可以去独读一下 groupcache 的源码,看看他是如何使用 duplication suppression 来实现一个 cache 库的

你可能感兴趣的:(golang)