本文主要说明如何用 Go 实现简单的本地缓存系统 SDK

1. 目标特性

  • ✅ 通用缓存接口(Set/Get/Delete)

  • ✅ 支持 TTL(过期自动清除)

  • ✅ 并发安全(支持高并发读写)

  • ✅ 易于集成到任意平台(封装为模块)

2. 简易版实现

localcache/
├── cache.go         # 外部可用接口
└── entry.go         # 缓存条目定义

✅ 1. 缓存条目定义 entry.go

package localcache

import "time"

type entry struct {
	value      interface{}
	expireAt   time.Time
	hasExpired bool
}

func (e *entry) isExpired() bool {
	if !e.hasExpired {
		return false
	}
	return time.Now().After(e.expireAt)
}

✅ 2. 缓存实现 cache.go

package localcache

import (
	"sync"
	"time"
)

type Cache struct {
	items map[string]*entry
	mu    sync.RWMutex
	ttl   time.Duration
	quit  chan struct{}
}

func NewCache(defaultTTL time.Duration) *Cache {
	c := &Cache{
		items: make(map[string]*entry),
		ttl:   defaultTTL,
		quit:  make(chan struct{}),
	}
	go c.cleanupLoop()
	return c
}

func (c *Cache) Set(key string, value interface{}, ttl ...time.Duration) {
	exp := c.ttl
	if len(ttl) > 0 {
		exp = ttl[0]
	}

	c.mu.Lock()
	defer c.mu.Unlock()
	c.items[key] = &entry{
		value:      value,
		expireAt:   time.Now().Add(exp),
		hasExpired: exp > 0,
	}
}

func (c *Cache) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	item, ok := c.items[key]
	if !ok || item.isExpired() {
		return nil, false
	}
	return item.value, true
}

func (c *Cache) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.items, key)
}

func (c *Cache) cleanupLoop() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			c.cleanExpired()
		case <-c.quit:
			return
		}
	}
}

func (c *Cache) cleanExpired() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	for k, v := range c.items {
		if v.hasExpired && now.After(v.expireAt) {
			delete(c.items, k)
		}
	}
}

func (c *Cache) Close() {
	close(c.quit)
}

✅ 3. 如何使用

cache := localcache.NewCache(10 * time.Minute)
defer cache.Close()

cache.Set("username", "huweihuang")
value, found := cache.Get("username")
if found {
    fmt.Println("Got:", value)
}

3. 增强版实现

基于最大容量的缓存,并使用 LRU(Least Recently Used,最近最少使用)策略进行淘汰

我们在前面提供的 Cache 实现基础上,引入 双向链表 + map 来实现高效的 O(1) 时间复杂度的 LRU 缓存淘汰机制。

✅ 增强目标

  • ✅ 支持最大容量限制 maxEntries

  • ✅ 最近使用的缓存项保留,最久未使用的优先被淘汰

  • ✅ 并发安全、支持 TTL 清理

  • Get / Set 都更新访问时间

✅ 实现结构

LRUCache
├── map[string]*list.Element     // 快速查找
├── list.List                    // 保存访问顺序,头是最近使用,尾是最久未使用
└── entry { key, value, expireAt }

✅ LRU 缓存结构代码

package lru

import (
	"container/list"
	"sync"
	"time"
)

type entry struct {
	key       string
	value     interface{}
	expireAt  time.Time
	ttlSet    bool
}

type LRUCache struct {
	capacity int
	mu       sync.Mutex
	ll       *list.List
	cache    map[string]*list.Element
	ttl      time.Duration
}

func NewLRUCache(capacity int, defaultTTL time.Duration) *LRUCache {
	return &LRUCache{
		capacity: capacity,
		ll:       list.New(),
		cache:    make(map[string]*list.Element),
		ttl:      defaultTTL,
	}
}

✅ 核心操作逻辑

Set

func (c *LRUCache) Set(key string, value interface{}, ttl ...time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if ele, ok := c.cache[key]; ok {
		// update existing
		en := ele.Value.(*entry)
		en.value = value
		en.expireAt = c.getExpireAt(ttl)
		en.ttlSet = true
		c.ll.MoveToFront(ele)
		return
	}

	// new entry
	en := &entry{
		key:      key,
		value:    value,
		expireAt: c.getExpireAt(ttl),
		ttlSet:   true,
	}
	ele := c.ll.PushFront(en)
	c.cache[key] = ele

	if c.ll.Len() > c.capacity {
		c.removeOldest()
	}
}

func (c *LRUCache) getExpireAt(ttl []time.Duration) time.Time {
	d := c.ttl
	if len(ttl) > 0 {
		d = ttl[0]
	}
	if d == 0 {
		return time.Time{}
	}
	return time.Now().Add(d)
}

Get

func (c *LRUCache) Get(key string) (interface{}, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()

	ele, ok := c.cache[key]
	if !ok {
		return nil, false
	}
	en := ele.Value.(*entry)
	if en.ttlSet && !en.expireAt.IsZero() && time.Now().After(en.expireAt) {
		c.removeElement(ele)
		return nil, false
	}

	c.ll.MoveToFront(ele)
	return en.value, true
}

removeOldest & removeElement

func (c *LRUCache) removeOldest() {
	ele := c.ll.Back()
	if ele != nil {
		c.removeElement(ele)
	}
}

func (c *LRUCache) removeElement(e *list.Element) {
	en := e.Value.(*entry)
	delete(c.cache, en.key)
	c.ll.Remove(e)
}

✅ 使用方式示例

cache := lru.NewLRUCache(100, 5*time.Minute)

cache.Set("a", "1")
cache.Set("b", "2")

val, ok := cache.Get("a") // 会变为“最近使用”

4. 原理

要理解这个 Go 实现的 本地 LRU 缓存系统 的原理,我们可以从两部分来讲:基本思想核心数据结构设计

🔧 4.1. 基本原理(逻辑流程)

📌 缓存的本质:

  • 把热点数据保存在内存中,提高读取速度。

  • 每个缓存项可能有「生命周期」(TTL)。

  • 当缓存空间满了,需要淘汰旧的数据

📌 LRU(Least Recently Used)策略:

  • 含义:优先淘汰最久没有被访问过的缓存项。

  • 适用场景:访问数据具有局部性原则(近期使用的数据可能会再次被访问)。

📌 怎么实现 LRU + TTL:

功能 实现方法
O(1) 访问 map[string]*list.Element 快速查找 key
O(1) 更新访问顺序 container/list 双向链表(LRU 队列)
O(1) 淘汰最旧项 从链表尾部直接删除
TTL 过期 每个 entry 存 expireAt 时间戳,访问时检查

🧠 4.2. 核心数据结构设计

1. Map + 双向链表

     LRU 队列(最近访问的放头部):

  [Most Recently Used] ---> [Middle] ---> [Least Recently Used]
          ↑                                ↑
         list.Front()                    list.Back()
type LRUCache struct {
	ll    *list.List                        // 双向链表:记录访问顺序
	cache map[string]*list.Element         // 快速定位 key -> 链表节点
}

2. 每个缓存 entry 包含信息

type entry struct {
	key       string
	value     interface{}
	expireAt  time.Time  // TTL 过期时间
	ttlSet    bool       // 是否设置了 TTL
}

⚙️ 4.3. 工作流程举例

1. 添加元素 Set("a", 1)

  • 如果 a 已存在:更新值,移动到链表头部。

  • 如果不存在:

    • 创建新 entry,放到链表头部。

    • 如果超过最大容量:删除链表尾部元素。

2. 访问元素 Get("a")

  • 找到 a 对应的链表节点。

  • 如果 TTL 未过期,移动它到链表头部(表示最近使用)。

3. 淘汰策略:

  • 添加新元素时,如果超过容量,就执行:
removeElement(list.Back()) // 淘汰最久未用的缓存项

🛡️ 4.4. 并发安全

  • 所有操作都使用 sync.Mutex 加锁。

  • 确保在多 goroutine 下不会出现竞争状态。

✅ 总结一句话:

这个缓存系统本质上是一个「哈希表 + 双向链表」的组合,用于快速定位 + 快速维护访问顺序,并结合 TTL 保证缓存不过期且高效清理。


最后修改 June 10, 2025: go cache (e8184b9)