当前位置: 首页 > 工具软件 > go-cache > 使用案例 >

Go 缓存系列 ——go-cache源码分析

颜文昌
2023-12-01

go-cache是基于内存的k/v缓存,类似于mencached,适用于单机应用程序。go-cache是线程安全的,支持并发地存取。虽然go-cache是保存在内存,但可以通过SaveFile()方法将缓存中的数据保存到文件。

一、数据结构

go-cache的逻辑代码主要是在cache.go文件中,首先来看它的数据结构

// 整体缓存
type Cache struct {
	*cache
}

type cache struct {
	defaultExpiration time.Duration  // 默认的过期时间
	items             map[string]Item  // 保存的k/v对
	mu                sync.RWMutex  // 读写锁
	onEvicted         func(string, interface{})  // delete操作后执行的回调函数
	janitor           *janitor
}

// 具体缓存的值
type Item struct {
	Object     interface{}
	Expiration int64  // 过期时间
}

 二、使用方法

1. 创建一个缓存

// 传入默认过期时间和清理时间
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
	items := make(map[string]Item)
	return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}

func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
	c := newCache(de, m)
	C := &Cache{c}
    // 如果传入的清理时间>0,则开启定期清理逻辑
	if ci > 0 {
		runJanitor(c, ci)
		runtime.SetFinalizer(C, stopJanitor)
	}
	return C
}

// 设定默认过期时间,初始化一个cache并返回
func newCache(de time.Duration, m map[string]Item) *cache {
	if de == 0 {
		de = -1
	}
	c := &cache{
		defaultExpiration: de,
		items:             m,
	}
	return c
}

2. Set

// 存储一个键值对
func (c *cache) Set(k string, x interface{}, d time.Duration) {
	
	var e int64
    // 设定过期时间:
    // 1. d = DefaultExpiration (即0),为默认过期时间
    // 2. d < 0,永不过期
    // 3. d > 0,采用传入的过期时间
	if d == DefaultExpiration {
		d = c.defaultExpiration
	}
	if d > 0 {
		e = time.Now().Add(d).UnixNano()
	}
    // 在将键值对写入map前先加锁
	c.mu.Lock()
	c.items[k] = Item{
		Object:     x,
		Expiration: e,
	}
	c.mu.Unlock()
}

3. Add

与Set类似,都是写入一个键值对,区别是当要存入的键已存在时,Add返回error,而Set覆盖原来的值

func (c *cache) Add(k string, x interface{}, d time.Duration) error {
	c.mu.Lock()
	_, found := c.get(k)
    // 如果键已存在则返回error
	if found {
		c.mu.Unlock()
		return fmt.Errorf("Item %s already exists", k)
	}
	c.set(k, x, d)
	c.mu.Unlock()
	return nil
}

4. Replace

替换已存在的键值,若键不存在则返回error

func (c *cache) Replace(k string, x interface{}, d time.Duration) error {
	c.mu.Lock()
	_, found := c.get(k)
    // 键不存在,返回error
	if !found {
		c.mu.Unlock()
		return fmt.Errorf("Item %s doesn't exist", k)
	}
	c.set(k, x, d)
	c.mu.Unlock()
	return nil
}

5. Get

func (c *cache) Get(k string) (interface{}, bool) {
    // 加读锁
	c.mu.RLock()
	item, found := c.items[k]
    // 没有找到则直接返回
	if !found {
		c.mu.RUnlock()
		return nil, false
	}
    // 如果设置了过期时间则检查是否过期,若过期了则直接返回
	if item.Expiration > 0 {
		if time.Now().UnixNano() > item.Expiration {
			c.mu.RUnlock()
			return nil, false
		}
	}
	c.mu.RUnlock()
	return item.Object, true
}

6. Delete

func (c *cache) Delete(k string) {
	c.mu.Lock()
	v, evicted := c.delete(k)
	c.mu.Unlock()
    // 如果注册了回调函数,则调用
	if evicted {
		c.onEvicted(k, v)
	}
}

// 执行删除键值对操作,并判断是否注册回调函数
func (c *cache) delete(k string) (interface{}, bool) {
	if c.onEvicted != nil {
		if v, found := c.items[k]; found {
			delete(c.items, k)
			return v.Object, true
		}
	}
	delete(c.items, k)
	return nil, false
}

7.定时清理逻辑

func runJanitor(c *cache, ci time.Duration) {
	j := &janitor{
		Interval: ci,
		stop:     make(chan bool),
	}
	c.janitor = j
	go j.Run(c)  // 新的协程做缓存清理逻辑
}

func (j *janitor) Run(c *cache) {
    // 定时器
	ticker := time.NewTicker(j.Interval)
	for {
		select {
        // 定时清理
		case <-ticker.C:
			c.DeleteExpired()
        // 终止清理
		case <-j.stop:
			ticker.Stop()
			return
		}
	}
}

// 与Delete类似,遍历所有键值对,找到过期的键值对执行删除操作,若该键值对有回调函数则一一调用
func (c *cache) DeleteExpired() {
	var evictedItems []keyAndValue
	now := time.Now().UnixNano()
	c.mu.Lock()
	for k, v := range c.items {
		if v.Expiration > 0 && now > v.Expiration {
			ov, evicted := c.delete(k)
			if evicted {
				evictedItems = append(evictedItems, keyAndValue{k, ov})
			}
		}
	}
	c.mu.Unlock()
	for _, v := range evictedItems {
		c.onEvicted(v.key, v.value)
	}
}

三、示例

package main

import (
	"fmt"
	"time"

	"github.com/patrickmn/go-cache"
)


func main() {
	c := cache.New(5 * time.Second, 10 * time.Second)
	c.OnEvicted(func(s string, i interface{}) {
		fmt.Printf("delete key: %s, value: %+v\n", s, i)
	})

	// 永不过期
	c.Set("test1", "never expired", cache.NoExpiration)
	// 默认过期时间
	c.Set("test2", "default expiration time", cache.DefaultExpiration)
	// 自定义过期时间
	c.Set("test3", "custom expiration time", 2 * time.Second)

	time.Sleep(3*time.Second)
	v1, found := c.Get("test1")
	fmt.Println("after 3 seconds")
	fmt.Printf("test1's value: %+v, found? %t\n", v1, found)
	v2, found := c.Get("test2")
	fmt.Printf("test2's value: %+v, found? %t\n", v2, found)
	v3, found := c.Get("test3")
	fmt.Printf("test3's value: %+v, found? %t\n", v3, found)

	time.Sleep(3*time.Second)
	v1, found = c.Get("test1")
	fmt.Println("after 6 seconds")
	fmt.Printf("test1's value: %+v, found? %t\n", v1, found)
	v2, found = c.Get("test2")
	fmt.Printf("test2's value: %+v, found? %t\n", v2, found)
	v3, found = c.Get("test3")
	fmt.Printf("test3's value: %+v, found? %t\n", v3, found)

	c.Delete("test1")
	v1, found = c.Get("test1")
	fmt.Printf("test1's value: %+v, found? %t\n", v1, found)
}


// 输出
// after 3 seconds
// test1's value: never expired, found? true
// test2's value: default expiration time, found? true
// test3's value: <nil>, found? false
// after 6 seconds
// test1's value: never expired, found? true
// test2's value: <nil>, found? false
// test3's value: <nil>, found? false
// delete key: test1, value: never expired
// test1's value: <nil>, found? false

 

 类似资料: