2. 单机并发缓存
...大约 4 分钟
https://github.com/dreamjz/golang-notes/tree/main/books/7-days-golang/GeeCache/day2-concurrency
DAY2-CONCURRENCY
│ byteview.go
│ cache.go
│ geecache.go
│ geecache_test.go
│ go.mod
│
└─lru
lru.go
lru_test.go
1. 支持并发读写
1.1 byteview
抽象出一个只读的数据结构ByteView
,用于表示缓存值。
// ByteView holds an immutable view of bytes
type ByteView struct {
b []byte
}
b []byte
:存储真实的缓存值;byte
类型能够支持所有的数据类型
// Len returns the view's length
func (v ByteView) Len() int {
return len(v.b)
}
// ByteSlice returns a copy of the data as byte slice
func (v ByteView) ByteSlice() []byte {
return cloneBytes(v.b)
}
// String returns the data as a string
func (v ByteView) String() string {
return string(v.b)
}
func cloneBytes(b []byte) []byte {
c := make([]byte, len(b))
copy(c, b)
return c
}
Len
:实现lru.Value
接口ByteSlice
:返回数据的拷贝,因为b
是只读的,防止外部程序意外修改缓存数据
1.2 cache
type cache struct {
mu sync.Mutex
lru *lru.Cache
cacheBytes int64
}
func (c *cache) add(key string, val ByteView) {
c.mu.Lock()
defer c.mu.Unlock()
if c.lru == nil {
c.lru = lru.New(c.cacheBytes, nil)
}
c.lru.Add(key, val)
}
func (c *cache) get(key string) (ByteView, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.lru == nil {
return ByteView{}, false
}
if v, ok := c.lru.Get(key); ok {
return v.(ByteView), ok
}
return ByteView{}, false
}
cache
:封装lru
的方法,为其添加互斥锁add
中若lru
为空才创建lru
的对象,被称为延迟初始化(Lazy Initialization),对象的创建延迟至第一次使用,可用于提高性能
2. 核心数据结构
核心数据结构Group
用于和用户交互,控制缓存值的存储和获取。
2.1 Getter 接口
// Getter loads data for a key
type Getter interface {
Get(key string) ([]byte, error)
}
var _ Getter = GetterFunc(nil)
// GetterFunc implements Getter with a function
type GetterFunc func(key string) ([]byte, error)
// Get implements Getter interface
func (f GetterFunc) Get(key string) ([]byte, error) {
return f(key)
}
- 接口
Getter
定义回调函数Get
,当缓存数据不存在,可以调用回调函数从数据源中获取 GetterFunc
:函数类型,实现了Getter
接口
接口型函数
若函数实现了某个接口,那么此函数被称为接口型函数。
此时即可以将接口型函数作为参数,也可以将实现了接口的结构体作为参数,使得函数的使用变得更加灵活多变。
func TestGetter(t *testing.T) {
var f Getter = GetterFunc(func(key string) ([]byte, error) {
return []byte(key), nil
})
want := []byte("key")
if v, _ := f.Get("key"); !reflect.DeepEqual(v, want) {
t.Errorf("callback failed")
}
}
2.2 Group
// Group is a cache namespace and associated data loaded spread over
type Group struct {
name string
getter Getter
mainCache cache
}
var (
mu sync.RWMutex
groups = make(map[string]*Group)
)
// NewGroup create a new instance of group
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {
if getter == nil {
panic("nil Getter")
}
mu.Lock()
defer mu.Unlock()
g := &Group{
name: name,
getter: getter,
mainCache: cache{cacheBytes: cacheBytes},
}
groups[name] = g
return g
}
func GetGroup(name string) *Group {
mu.RLock()
defer mu.RUnlock()
g := groups[name]
return g
}
- 一个
Group
表示一个缓存的命名空间:name
:Group
的名称getter
:缓存未命中时获取源数据的回调(callback)maincache
:并发缓存
NewGroup
:创建新的Group
实例,添加到全局变量groups
中GetGroup
:获取指定的Group
实例,此处使用了读锁mu
:全局变量,读写锁groups
:全局变量,哈希表存储所有的Group
实例
func (g *Group) Get(key string) (ByteView, error) {
if key == "" {
return ByteView{}, fmt.Errorf("key is required")
}
if v, ok := g.mainCache.get(key); ok {
log.Println("[GeeCache] hit")
return v, nil
}
return g.load(key)
}
func (g *Group) load(key string) (ByteView, error) {
return g.getLocally(key)
}
func (g *Group) getLocally(key string) (ByteView, error) {
bytes, err := g.getter.Get(key)
if err != nil {
return ByteView{}, err
}
val := ByteView{b: cloneBytes(bytes)}
g.populateCache(key, val)
return val, nil
}
func (g *Group) populateCache(key string, val ByteView) {
g.mainCache.add(key, val)
}
Get
流程如下:
- 从缓存中获取数据,若存在则返回,否则继续 步骤 2)
- 从数据源中获取数据,
getLocally
调用回调函数g.getter.Get
获取源数据 - 将数据添加至缓存中
- 返回数据
3. 测试
使用哈希表模拟数据库:
var db = map[string]string{
"Tom": "630",
"Jack": "589",
"Sam": "567",
}
func TestGet(t *testing.T) {
loadCount := make(map[string]int, len(db))
gee := NewGroup("scores", 2<<10, GetterFunc(
func(key string) ([]byte, error) {
log.Println("[SlowDB] search key", key)
if v, ok := db[key]; ok {
if _, ok := loadCount[key]; !ok {
loadCount[key] = 0
}
loadCount[key] += 1
return []byte(v), nil
}
return nil, fmt.Errorf("%s not exist", key)
},
))
for k, v := range db {
if view, err := gee.Get(k); err != nil || view.String() != v {
t.Fatal("failed to get val from cache")
}
if _, err := gee.Get(k); err != nil || loadCount[k] > 1 {
t.Fatalf("cache %s miss", k)
}
}
if view, err := gee.Get("unknown"); err == nil {
t.Fatalf("want empty val, got %s", view)
}
}
16:27:13 [SlowDB] search key Tom
16:27:13 [GeeCache] hit
16:27:13 [SlowDB] search key Jack
16:27:13 [GeeCache] hit
16:27:13 [SlowDB] search key Sam
16:27:13 [GeeCache] hit
16:27:13 [SlowDB] search key unknown
Reference
Powered by Waline v2.15.2