gcache-一个用go实现的内存库

简介

gcache是一个使用Go语言实现的内存缓存库,在github上有2.5k个star,支持LFU、LRU、ARC等多种缓存驱逐策略,可有利于对相对应思想的实现。

但该库最近的一次更新已经是2021年的,所以该库不推荐用于实际生产,仅作为源码学习使用。

根据KHWL图标来列举出一些知识点以及我想要学习的地方,如下图:

快速使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"github.com/bluele/gcache"
"fmt"
)

func main() {
gc := gcache.New(20).
LRU().
Build()
gc.Set("key", "ok")
value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
}

运行结果:

1
Get: ok

我们调用了gcache库的New方法,传入一个int值,然后采用链式调用的方法,调用LRU方法,最后调用Build方法来初始化一个对象。然后使用该对象设置键值对,最后获取并打印。

在上面代码中可以看出,初始化对象时该库使用了建造者模式,只有一个必传参数也就是New方法的size参数,该参数代表内存中可存储的最大键值对数量,其他参数都可通过链式方法进行调用。

说明
在Go中,除了这种建造者模式,还可以通过闭包来实现函数式传参。

获取对象之后,就像其他内存库一样,调用Set和Get方法即可设置和获取键值对。

缓存过期时间

在使用该库去设置键值对的时候,默认是不会过期的,除非你显示的调用SetWithExpire方法并传入time.Duration参数,这样才会有指定的时间将缓存删除掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"github.com/bluele/gcache"
"fmt"
"time"
)

func main() {
gc := gcache.New(20).
LRU().
Build()
gc.SetWithExpire("key", "ok", time.Second*10)
value, _ := gc.Get("key")
fmt.Println("Get:", value)

// Wait for value to expire
time.Sleep(time.Second*10)

value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
}

输出:

1
2
3
Get: ok
// 10 seconds later, new attempt:
panic: ErrKeyNotFound

可以看出在设置key同时指定了一个过期时间,在等待了指定时间后再去获取,该key已经从内存中删除了。

当指定数据不存在时自动获取

当获取一个不存在的key时,支持自动的去设置该key的value。为了实现这个效果,gcache暴露了一个LoaderFunc类型,在初始化缓存对象时通过链式调用进行传入,这是它的定义:

1
2
3
4
type (  
LoaderFunc func(interface{}) (interface{}, error)
// 其他类型
)

这是一个函数类型,参数是对应不存在值的key,返回一个数据和error,下面是使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"github.com/bluele/gcache"
"fmt"
)

func main() {
gc := gcache.New(20).
LRU().
LoaderFunc(func(key interface{}) (interface{}, error) {
return "ok", nil
}).
Build()
value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
}

从上面代码可以看出,在创建缓存对象时传入了一个LoaderFunc匿名函数,始终返回ok字符串和空错误,所以在获取key这个对象时,会直接返回ok。

1
Get: ok

在此基础上,同样支持对指定key和value的expire过期时间,使用方式就是将上面的LoaderFunc方法替换成LoaderExpireFunc方法,该方法会多添加一个返回值,就是该key的过期时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"time"

"github.com/bluele/gcache"
)

func main() {
var evictCounter, loaderCounter, purgeCounter int
gc := gcache.New(20).
LRU().
LoaderExpireFunc(func(key interface{}) (interface{}, *time.Duration, error) {
expire := 1 * time.Second
return "ok", &expire, nil
}).
Build()
value, err := gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
time.Sleep(1 * time.Second)
value, err = gc.Get("key")
if err != nil {
panic(err)
}
fmt.Println("Get:", value)
}

输出:

1
2
Get: ok
Get: ok

内存缓存的一些通用操作它都有,比如删除、所有key、获取缓存中的key的数量、删除所有key、判断一个key是否在缓存中等等。

源码实现

CacheBuilder

首先从库的包级函数New入手,该函数返回CacheBuilder结构体,用于构建Cache对象,对象字段如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type CacheBuilder struct {  
clock Clock
tp string
size int
loaderExpireFunc LoaderExpireFunc
evictedFunc EvictedFunc
purgeVisitorFunc PurgeVisitorFunc
addedFunc AddedFunc
expiration *time.Duration
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
}

func New(size int) *CacheBuilder {
return &CacheBuilder{
clock: NewRealClock(),
tp: TYPE_SIMPLE,
size: size,
}
}

对应链式调用函数就将该对象的字段设置成入参,然后返回该对象指针,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (cb *CacheBuilder) Simple() *CacheBuilder {  
return cb.EvictType(TYPE_SIMPLE)
}

func (cb *CacheBuilder) LRU() *CacheBuilder {
return cb.EvictType(TYPE_LRU)
}

func (cb *CacheBuilder) LFU() *CacheBuilder {
return cb.EvictType(TYPE_LFU)
}

func (cb *CacheBuilder) ARC() *CacheBuilder {
return cb.EvictType(TYPE_ARC)
}

上述代码设置该缓存对象的过期对象驱逐策略,将EvictType设置成对应的常量,然后调用Build方法开始构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (cb *CacheBuilder) Build() Cache {  
// 参数校验
if cb.size <= 0 && cb.tp != TYPE_SIMPLE {
panic("gcache: Cache size <= 0")
}

return cb.build()
}

// Cache是一个接口,包含了对缓存的通用操作
func (cb *CacheBuilder) build() Cache {
// 根据驱逐策略选择的不同去实例化不同对象
switch cb.tp {
case TYPE_SIMPLE:
return newSimpleCache(cb)
case TYPE_LRU:
return newLRUCache(cb)
case TYPE_LFU:
return newLFUCache(cb)
case TYPE_ARC:
return newARC(cb)
default:
panic("gcache: Unknown type " + cb.tp)
}
}

如果不选择驱逐策略,那么就是默认的TYPE_SIMPLE类型,则不驱逐缓存。

Cache

在gcache中定义了一个接口Cache,一个驱逐策略就对应一个结构体,实现该接口,以下是接口的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Cache interface {  
// Set inserts or updates the specified key-value pair.
Set(key, value interface{}) error
// SetWithExpire inserts or updates the specified key-value pair with an expiration time.
SetWithExpire(key, value interface{}, expiration time.Duration) error
// Get returns the value for the specified key if it is present in the cache. // If the key is not present in the cache and the cache has LoaderFunc,
// invoke the `LoaderFunc` function and inserts the key-value pair in the cache. // If the key is not present in the cache and the cache does not have a LoaderFunc,
// return KeyNotFoundError.
Get(key interface{}) (interface{}, error)
// GetIFPresent returns the value for the specified key if it is present in the cache.
// Return KeyNotFoundError if the key is not present.
GetIFPresent(key interface{}) (interface{}, error)

// GetAll returns a map containing all key-value pairs in the cache.
GetALL(checkExpired bool) map[interface{}]interface{}
get(key interface{}, onLoad bool) (interface{}, error)
// Remove removes the specified key from the cache if the key is present.
// Returns true if the key was present and the key has been deleted.
Remove(key interface{}) bool
// Purge removes all key-value pairs from the cache.
Purge()
// Keys returns a slice containing all keys in the cache.
Keys(checkExpired bool) []interface{}
// Len returns the number of items in the cache.
Len(checkExpired bool) int
// Has returns true if the key exists in the cache.
Has(key interface{}) bool
}

定义了内存库中的一些常规操作。

SimpleCache

在初始化缓存对象时如果指定具体的内存驱逐策略,则使用SimpleCache结构来作为默认的缓存对象,这个对象结构比较简单,处理在建造者模式中获取到的其他数据之外,还有一个map类型的变量items来存放对应的键值对,value是一个包装后的simpleItem结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type baseCache struct {  
clock Clock
size int
loaderExpireFunc LoaderExpireFunc
evictedFunc EvictedFunc
purgeVisitorFunc PurgeVisitorFunc
addedFunc AddedFunc
deserializeFunc DeserializeFunc
serializeFunc SerializeFunc
expiration *time.Duration
mu sync.RWMutex
loadGroup Group
*stats
}

type simpleItem struct {
clock Clock
value interface{}
// 这个value的过期时间
expiration *time.Time
}

// SimpleCache has no clear priority for evict cache. It depends on key-value map order.
type SimpleCache struct {
baseCache
items map[interface{}]*simpleItem
}

SimpleCache对象实现了Cache接口,现在来看这个库设置key时是如何处理过期时间的,在调用Set()和SetWithExpirce()方法时,最终都会调用simpleCache.set()方法,只不过这两者的区别是SetWithExpirce方法会计算过期时间,并将时间设置到simpleItem对象的expiration字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Set a new key-value pair
func (c *SimpleCache) Set(key, value interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.set(key, value)
return err
}

// Set a new key-value pair with an expiration time
func (c *SimpleCache) SetWithExpire(key, value interface{}, expiration time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
item, err := c.set(key, value)
if err != nil {
return err
}
// 计算这个key的过期时间,并将该时间设置到simpleItem的expiration字段
t := c.clock.Now().Add(expiration)
item.(*simpleItem).expiration = &t
return nil
}

// simpleCache的核心逻辑函数
func (c *SimpleCache) set(key, value interface{}) (interface{}, error) {
var err error
// 查看这个缓存是否有设置自己的序列化函数,有的话就序列化,反之跳过
if c.serializeFunc != nil {
value, err = c.serializeFunc(key, value)
if err != nil {
return nil, err
}
}

// Check for existing item
item, ok := c.items[key]
// 查看该key是否已经在缓存中,如果有则更新
if ok {
item.value = value
} else {
// 没有的话则要判断当前缓存是否已经满了,满了的话则要驱逐
if (len(c.items) >= c.size) && c.size > 0 {
// 驱逐一个缓存
c.evict(1)
}
item = &simpleItem{
clock: c.clock,
value: value,
}
c.items[key] = item
}

if c.expiration != nil {
t := c.clock.Now().Add(*c.expiration)
item.expiration = &t
}

if c.addedFunc != nil {
c.addedFunc(key, value)
}

return item, nil
}

上面的逻辑也很简单,先判断是否设置了序列化函数,如果有则先将值进行序列化。然后判断该key数据是否在缓存中存在,如果存在则替换,不存在的话判断当前缓存的数量是否已经满了,满了则驱逐一个,否则直接添加。

然后判断是否有自定义添加key的回调函数,有则调用。

那在SimpleCache是如何判断该key过期的呢,是在simpleItem结构体里定义了一个IsExpired方法,传入一个time.Time类型。

map中对应存储的value是一个simpleItem结构,并且存储了expiration字段作为该值的过期时间。调用time.Time的Before方法,来判断过期时间是否在传入时间之前,返回该布尔值,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 外部应用调用的方法
func (c *SimpleCache) Get(key interface{}) (interface{}, error) {
v, err := c.get(key, false)
if err == KeyNotFoundError {
return c.getWithLoader(key, true)
}
return v, err
}

func (c *SimpleCache) get(key interface{}, onLoad bool) (interface{}, error) {
// 获取key
v, err := c.getValue(key, onLoad)
if err != nil {
return nil, err
}
// 解码
if c.deserializeFunc != nil {
return c.deserializeFunc(key, v)
}
return v, nil
}

func (c *SimpleCache) getValue(key interface{}, onLoad bool) (interface{}, error) {
c.mu.Lock()
item, ok := c.items[key]
if ok {
// 判断
if !item.IsExpired(nil) {
v := item.value
c.mu.Unlock()
if !onLoad {
c.stats.IncrHitCount()
}
return v, nil
}
c.remove(key)
}
c.mu.Unlock()
if !onLoad {
c.stats.IncrMissCount()
}
return nil, KeyNotFoundError
}

// IsExpired returns boolean value whether this item is expired or not.
func (si *simpleItem) IsExpired(now *time.Time) bool {
if si.expiration == nil {
return false
}
if now == nil {
t := si.clock.Now()
now = &t
}
// 判断是否过期
return si.expiration.Before(*now)
}

还有一个点,那就是simpleCache在键值对达到上限之后,是如何驱逐内存的。在上面的代码上可以看出,是调用了evict函数,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
func (c *SimpleCache) evict(count int) {  
now := c.clock.Now()
current := 0
for key, item := range c.items {
if current >= count {
return
}
if item.expiration == nil || now.After(*item.expiration) {
defer c.remove(key)
current++
}
}
}

从代码中可以看出,就是遍历该map,然后将遍历到的第一个过期的value删除。

LFU

LFU(最少使用)算法会淘汰缓存中访问次数最少的数据。
其核心原则是,如果数据过去被访问多次,那么将来被访问的频率也会更高。
在 LFU 实现中,通常需要维护一个按照访问次数排序的队列,每次访问时,次数加1,队伍重新排序,淘汰时选择访问次数最少的即可。

在gcache中,选择是用list来存放访问频率,并且类似simpleCache一样,将存入的数据封装成一个对象,该对象也包含了该list节点的指针,这样被存放的对象和存放频率的list对象就能相互引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
type lfuItem struct {  
clock Clock
key interface{}
value interface{}
// 这里的list是标准库实现包
freqElement *list.Element
expiration *time.Time
}

// 链表中实际存放的元素类型
type freqEntry struct {
// 代表访问频率
freq uint
// 当前访问频率的元素
items map[*lfuItem]struct{}
}

// 该方法会初始化LFU cache的时候调用
func (c *LFUCache) init() {
c.freqList = list.New()
c.items = make(map[interface{}]*lfuItem, c.size)
c.freqList.PushFront(&freqEntry{
freq: 0,
items: make(map[*lfuItem]struct{}),
})
}

func (c *LFUCache) set(key, value interface{}) (interface{}, error) {
// 精简掉一些不关主流程的代码
// Check for existing item
item, ok := c.items[key]
if ok {
item.value = value
} else {
// 如果当前大小大于预先设定好的数量,则驱逐一个
if len(c.items) >= c.size {
c.evict(1)
}
item = &lfuItem{
clock: c.clock,
key: key,
value: value,
freqElement: nil,
}
// 获取链表第一个节点
el := c.freqList.Front()
// 进行类型断言,获取到正确类型对象
fe := el.Value.(*freqEntry)
// 在当前访问频率的对象中将新增节点添加
fe.items[item] = struct{}{}

// 然后将代表访问频率的指针对象也赋值给value对象
item.freqElement = el
c.items[key] = item
}

// 精简掉一些不关主流程的代码
return item, nil
}

从上面代码可以看出,在新增一个缓存对象时,将缓存对象包装成lfuItem类型,因为是新增,所以访问频率是1,从LFUCache类型对象里获取链表的第一个节点(即访问频率为0的节点),该链表节点实际存储的类型是freqEntry类型,使用一个map来存储当前访问频率的元素集合,将新增元素添加到map中。最后也将该链表节点的指针赋值给lfuItem。

为什么要将链表节点的指针赋值给lfuItem中呢?因为要在后面更新频率是用到。gcache是如何更新访问频率的呢?

访问一次缓存就会更新频率,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func (c *LFUCache) getValue(key interface{}, onLoad bool) (interface{}, error) {  
c.mu.Lock()
item, ok := c.items[key]
if ok {
// 缓存中存在这个key并且没有过期时才会更新频率
if !item.IsExpired(nil) {
// 更新频率的实现函数
c.increment(item)
v := item.value
c.mu.Unlock()
// 精简其他代码
return v, nil
}
// 缓存存在该key但过期了,那就删除该项
c.removeItem(item)
}
// 精简其他代码
return nil, KeyNotFoundError
}

// 实际更新频率的函数
func (c *LFUCache) increment(item *lfuItem) {
// 获取缓存对象所对应的链表节点
currentFreqElement := item.freqElement
// 将链表节点通过类型断言转换成freqEntry
currentFreqEntry := currentFreqElement.Value.(*freqEntry)
// 访问频率加1
nextFreq := currentFreqEntry.freq + 1
// 从链表节点的map中删除该缓存对象,因为它的访问频率已经加1了
delete(currentFreqEntry.items, item)

// a boolean whether reuse the empty current entry
// 判断这个链表节点是否要被删除,判断条件是当前节点的freq不等于0并且该节点的map长度为0
removable := isRemovableFreqEntry(currentFreqEntry)

// 获取当前节点的下一个节点
nextFreqElement := currentFreqElement.Next()
switch {
case nextFreqElement == nil || nextFreqElement.Value.(*freqEntry).freq > nextFreq:
// 如果当前节点的下一个节点是nil的
// 或者当前节点的下一个节点的频率大于预期频率
if removable {
// 当前节点的freq不为0并且该节点的map长度为0
// 用预期频率替换掉当前节点的freq
currentFreqEntry.freq = nextFreq
nextFreqElement = currentFreqElement
} else {
// 当前节点还有其他缓存所以不删除,在当前节点的后面新增一个节点
nextFreqElement = c.freqList.InsertAfter(&freqEntry{
freq: nextFreq,
items: make(map[*lfuItem]struct{}),
}, currentFreqElement)
}
case nextFreqElement.Value.(*freqEntry).freq == nextFreq:
// 当前节点的下一个节点的freq正好是预期频率
if removable {
c.freqList.Remove(currentFreqElement)
}
default:
panic("unreachable")
}
// 然后在链表节点里的map添加缓存对象
nextFreqElement.Value.(*freqEntry).items[item] = struct{}{}
item.freqElement = nextFreqElement
}

从上面代码可以看出,从缓存对象中获取链表节点指针,将指针通过类型断言转换成freqEntry类型,然后将缓存对象从freqEntry类型中的map中删除,把频率自增1并赋值给 nextFreq。

再获取当前链表节点的下一个节点,判断是否为nil 或者 下一个节点的 freq 值大于 nextFreq,再根据当前链表节点是否要删除再选择是要插入一个新节点还是在当前节点添加。

然后在更新下缓存对象和链表节点的双向指针引用,这样就完成了更新频率。

访问频率低的总会是在链表的第一个节点,所以在开始驱逐的时候,就从链表第一个节点开始删除,链表节点存放这一个map,代表当前频率的缓存对象,然后遍历这个map,将获取到的元素删除掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (c *LFUCache) evict(count int) {  
entry := c.freqList.Front()
for i := 0; i < count; {
if entry == nil {
return
} else {
for item := range entry.Value.(*freqEntry).items {
if i >= count {
return
}
c.removeItem(item)
i++
}
entry = entry.Next()
}
}
}

func (c *LFUCache) removeItem(item *lfuItem) {
entry := item.freqElement.Value.(*freqEntry)
delete(c.items, item.key)
delete(entry.items, item)
if isRemovableFreqEntry(entry) {
c.freqList.Remove(item.freqElement)
}
if c.evictedFunc != nil {
c.evictedFunc(item.key, item.value)
}
}

LRU

LRU(最近最少使用)算法会在缓存写满的时候,会根据所有数据的访问记录,淘汰掉未来被访问几率最低的数据。

阅读源码时发现,其核心逻辑和LFU大体上一致,只不过LRU不用维护访问频率链表,转而维护一个存放缓存对象的双向链表。当获取缓存时,将代表缓存对象的链表对象移动到链表头部。后面开始删除缓存时,获取最后链表的最后一个元素删除即可。

LRU的列表定义:

1
2
3
4
5
6
7
8
// Discards the least recently used items first.  
type LRUCache struct {
baseCache
// 存放key和value的对应关系
items map[interface{}]*list.Element
// 存放所有value的链表
evictList *list.List
}

设置缓存对象的调用逻辑和LFU差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (c *LRUCache) set(key, value interface{}) (interface{}, error) {  
var err error
// 精简代码

// Check for existing item
var item *lruItem
if it, ok := c.items[key]; ok {
// 该缓存存在,则将该缓存对应的链表对象,移动到链表的头部
c.evictList.MoveToFront(it)
item = it.Value.(*lruItem)
item.value = value
} else {
// Verify size not exceeded
// 该缓存对象没有存在
// 判断当前的缓存池是否已经满了,如果满了则驱逐一个
if c.evictList.Len() >= c.size {
c.evict(1)
}
// 构造缓存对象
item = &lruItem{
clock: c.clock,
key: key,
value: value,
}
// 将该缓存对象移动到头部,然后设置到map中
// 设置时也代表着该缓存是活跃的,所以会移动到头部。
c.items[key] = c.evictList.PushFront(item)
}
// 精简代码
return item, nil
}

那驱逐时是怎么驱逐的?这里很简单,直接获取到链表的尾部元素,直接驱逐即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// evict removes the oldest item from the cache.func (c *LRUCache) evict(count int) {  
for i := 0; i < count; i++ {
ent := c.evictList.Back()
if ent == nil {
return
} else {
c.removeElement(ent)
}
}
}

func (c *LRUCache) removeElement(e *list.Element) {
c.evictList.Remove(e)
entry := e.Value.(*lruItem)
delete(c.items, entry.key)
if c.evictedFunc != nil {
entry := e.Value.(*lruItem)
c.evictedFunc(entry.key, entry.value)
}
}

获取缓存对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (c *LRUCache) getValue(key interface{}, onLoad bool) (interface{}, error) {  
c.mu.Lock()
item, ok := c.items[key]
// 判断该缓存对象是否已经存在
if ok {
it := item.Value.(*lruItem)
// 判断该缓存对象是否已经到期
if !it.IsExpired(nil) {
// 没有到期则将链表节点移动到头部并返回
c.evictList.MoveToFront(item)
v := it.value
c.mu.Unlock()
if !onLoad {
c.stats.IncrHitCount()
}
return v, nil
}
// 该缓存已经过期,删除
c.removeElement(item)
}
c.mu.Unlock()
if !onLoad {
c.stats.IncrMissCount()
}
// 走到这就意味着缓存已经过期,所以返回该key找不到的错误
return nil, KeyNotFoundError
}

缓存过期时间的实现

缓存过期时间的实现比较简单,在生成缓存对象的时候会生成过期时间的Time对象,然后在获取缓存的时候,判断该缓存对象的Time对象是否已经过期,过期了就删除该对象并且返回KeyNotFoundErr错误,否则返回该对象。

从上面章节我们知道,gcache定义了一个缓存接口,包含了许多缓存操作方法,每种缓存驱逐策略都实现了这个接口,每种实现对于缓存过期时间,都是一种方法,所以我就贴出其中一种。下面是LRU的缓存过期时间实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type lruItem struct {  
clock Clock
key interface{}
value interface{}
expiration *time.Time
}

// IsExpired returns boolean value whether this item is expired or not.func (it *lruItem) IsExpired(now *time.Time) bool {
if it.expiration == nil {
return false
}
if now == nil {
t := it.clock.Now()
now = &t
}
return it.expiration.Before(*now)
}

gcache-一个用go实现的内存库
https://sunshinechen.cn/2024/03/13/gcache,一个用go实现的内存库/
作者
sunshine
发布于
2024年3月13日
许可协议