高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全

高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全

在高并发服务开发中,内存缓存是提升性能的关键组件。而支持自动过期(TTL) 的缓存更是常见需求。本文将介绍一种基于 Go 语言实现的高性能、并发安全、支持过期时间自动刷新的内存缓存方案,并附完整源码、测试用例及性能分析。

🎯 设计目标

  • 所有条目使用统一的全局 TTL(Time-To-Live)
  • Set(key, value) 会重置该 key 的过期时间
  • 过期后自动删除,无需手动清理
  • 并发安全:支持多 goroutine 同时读写
  • 高性能:读操作零分配,写操作开销可控
  • 精确过期:避免"提前误删"或"延迟删除"

🧠 核心思路:为每个 Key 绑定独立 Timer

传统方案常使用后台协程轮询 + 堆(heap)管理过期时间,但存在两个问题:

  1. 同一个 key 多次 Set 会导致堆中冗余项;
  2. 旧的过期计划可能误删刚被刷新的 key。

我们采用更简洁、精确的方案:

每次 Set 时,为该 key 启动一个 time.Timer,在 TTL 后自动删除。若 key 被再次 Set,则取消旧 Timer,启动新 Timer。

Go 的 time.Timer 由 runtime 高效管理,轻量且精确,非常适合此场景。

📦 完整源码 (cache.go)

go 复制代码
// cache.go
package timecache

import (
	"sync"
	"time"
)

// Cache is a thread-safe in-memory cache with fixed TTL.
type Cache[T any] struct {
	items  map[string]T
	timers map[string]*time.Timer
	mu     sync.RWMutex
	ttl    time.Duration
}

// NewCache creates a new cache with the given TTL.
// Panics if ttl <= 0.
func NewCache[T any](ttl time.Duration) *Cache[T] {
	if ttl <= 0 {
		panic("TTL must be positive")
	}
	return &Cache[T]{
		items:  make(map[string]T),
		timers: make(map[string]*time.Timer),
		ttl:    ttl,
	}
}

// Set stores a value and resets its expiration timer.
func (c *Cache[T]) Set(key string, value T) {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Cancel existing timer if any
	if timer, exists := c.timers[key]; exists {
		timer.Stop()
		delete(c.timers, key)
	}

	// Store value
	c.items[key] = value

	// Schedule expiration
	timer := time.AfterFunc(c.ttl, func() {
		c.mu.Lock()
		defer c.mu.Unlock()
		// Double-check existence to avoid race
		if _, exists := c.items[key]; exists {
			delete(c.items, key)
			// timers[key] already deleted above, but safe to double-delete
		}
	})

	c.timers[key] = timer
}

// Get returns the value if it exists and has not expired.
func (c *Cache[T]) Get(key string) (T, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	if val, ok := c.items[key]; ok {
		return val, true
	}
	var zero T
	return zero, false
}

// Delete removes a key immediately and cancels its timer.
func (c *Cache[T]) Delete(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.items, key)
	if timer, ok := c.timers[key]; ok {
		timer.Stop()
		delete(c.timers, key)
	}
}

// Len returns the current number of items (may include items about to expire).
func (c *Cache[T]) Len() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.items)
}

💡 包名建议为 timecache(非 main),便于复用。

🧪 全面测试 (cache_test.go)

go 复制代码
// cache_test.go
package timecache

import (
	"sync"
	"testing"
	"time"
)

func TestCache_SetAndGet(t *testing.T) {
	cache := NewCache[string](100 * time.Millisecond)
	cache.Set("key1", "value1")
	if val, ok := cache.Get("key1"); !ok || val != "value1" {
		t.Errorf("Expected value1, got %v, ok=%v", val, ok)
	}
}

func TestCache_Expired(t *testing.T) {
	cache := NewCache[string](50 * time.Millisecond)
	cache.Set("key1", "value1")
	time.Sleep(60 * time.Millisecond)
	if _, ok := cache.Get("key1"); ok {
		t.Error("Expected key1 to be expired")
	}
}

func TestCache_RefreshOnSet(t *testing.T) {
	cache := NewCache[string](100 * time.Millisecond)
	cache.Set("key1", "value1")
	time.Sleep(60 * time.Millisecond)        // 60ms passed
	cache.Set("key1", "value2")             // refresh
	time.Sleep(50 * time.Millisecond)        // total 110ms < 160ms
	if val, ok := cache.Get("key1"); !ok || val != "value2" {
		t.Errorf("Key should be alive after refresh, got ok=%v, val=%v", ok, val)
	}
	time.Sleep(60 * time.Millisecond)        // total 170ms > 160ms
	if _, ok := cache.Get("key1"); ok {
		t.Error("Key should be expired after refreshed TTL")
	}
}

func TestCache_Delete(t *testing.T) {
	cache := NewCache[string](1 * time.Second)
	cache.Set("key1", "value1")
	cache.Delete("key1")
	if _, ok := cache.Get("key1"); ok {
		t.Error("Key should be deleted immediately")
	}
}

func TestCache_Len(t *testing.T) {
	cache := NewCache[string](1 * time.Second)
	cache.Set("k1", "v1")
	cache.Set("k2", "v2")
	if cache.Len() != 2 {
		t.Errorf("Expected len=2, got %d", cache.Len())
	}
	cache.Delete("k1")
	if cache.Len() != 1 {
		t.Errorf("Expected len=1 after delete, got %d", cache.Len())
	}
	time.Sleep(1100 * time.Millisecond)
	if cache.Len() != 0 {
		t.Errorf("Expected len=0 after expiration, got %d", cache.Len())
	}
}

func TestCache_ConcurrentSetGet(t *testing.T) {
	cache := NewCache[int](100 * time.Millisecond)
	const N = 1000
	var wg sync.WaitGroup
	wg.Add(2)

	go func() {
		defer wg.Done()
		for i := 0; i < N; i++ {
			cache.Set("shared", i)
			time.Sleep(time.Microsecond)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < N; i++ {
			cache.Get("shared")
			time.Sleep(time.Microsecond)
		}
	}()
	wg.Wait()
}

func TestCache_ConcurrentSetDelete(t *testing.T) {
	cache := NewCache[int](100 * time.Millisecond)
	const N = 500
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			key := "key" + string(rune('0'+id))
			for j := 0; j < N; j++ {
				cache.Set(key, j)
				cache.Delete(key)
			}
		}(i)
	}
	wg.Wait()
}

func TestCache_ZeroOrNegativeTTL(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Error("Expected panic for zero/negative TTL")
		}
	}()
	_ = NewCache[string](-1 * time.Second)
}

// ----------------------------
// Benchmarks
// ----------------------------

func BenchmarkCache_Set(b *testing.B) {
	cache := NewCache[int](1 * time.Hour)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		i := 0
		for pb.Next() {
			cache.Set("key", i)
			i++
		}
	})
}

func BenchmarkCache_Get(b *testing.B) {
	cache := NewCache[int](1 * time.Hour)
	cache.Set("key", 42)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			cache.Get("key")
		}
	})
}

func BenchmarkCache_SetGet(b *testing.B) {
	cache := NewCache[int](1 * time.Hour)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		i := 0
		for pb.Next() {
			key := "key" + string(rune(i%100))
			cache.Set(key, i)
			cache.Get(key)
			i++
		}
	})
}

func BenchmarkCache_WithExpiration(b *testing.B) {
	cache := NewCache[int](10 * time.Millisecond)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		i := 0
		for pb.Next() {
			key := "key" + string(rune(i%10))
			cache.Set(key, i)
			time.Sleep(time.Microsecond)
			i++
		}
	})
	time.Sleep(20 * time.Millisecond)
}

func BenchmarkCache_Memory(b *testing.B) {
	b.ReportAllocs()
	cache := NewCache[int](1 * time.Hour)
	for i := 0; i < b.N; i++ {
		cache.Set("key"+string(rune(i%1000)), i)
	}
}

📊 性能测试结果

测试环境:

  • OS: Windows
  • CPU: AMD Ryzen 7 5700U (8核16线程)
  • Go: 1.22+
text 复制代码
goos: windows
goarch: amd64
pkg: timeCache
cpu: AMD Ryzen 7 5700U with Radeon Graphics         
BenchmarkCache_Set-16               	 2756146	       419.4 ns/op	     160 B/op	       2 allocs/op
BenchmarkCache_Get-16               	56984385	        21.64 ns/op	       0 B/op	       0 allocs/op
BenchmarkCache_SetGet-16            	 1858731	       707.8 ns/op	     164 B/op	       3 allocs/op
BenchmarkCache_WithExpiration-16    	   28728	     35379 ns/op	     164 B/op	       3 allocs/op
BenchmarkCache_Memory-16            	 3255846	       355.5 ns/op	     166 B/op	       3 allocs/op
PASS
ok  	timeCache	7.635s

🔍 性能解读

操作 延迟 分配 评价
Get 21.6 ns 0 B ⭐⭐⭐⭐⭐ 接近裸 map,零分配
Set 419 ns 160 B ✅ 合理(含 timer 创建)
Set+Get 708 ns 164 B ✅ 高并发下表现稳定
带过期写入 35.4 μs 164 B ⚠️ 含 1μs sleep,实际 timer 开销可控

💡 结论:读性能极佳,写性能在可接受范围内,完全满足 Web 服务、API 缓存等场景。

✅ 优势总结

特性 说明
精确过期 每个 key 独立 timer,无误删
自动刷新 多次 Set 自动重置 TTL
并发安全 RWMutex 保护,支持高并发
零 GC 压力(读路径) Get 操作无堆分配
代码简洁 核心逻辑 < 50 行,易于维护
泛型支持 Go 1.18+ 类型安全

🚀 适用场景

  • Web API 响应缓存
  • 用户会话(Session)存储
  • 限流令牌桶(Token Bucket)状态
  • 短期配置缓存
  • 高频读、低频写的业务数据

📌 结语

本文实现的缓存组件在正确性、性能、简洁性之间取得了良好平衡。通过 time.Timer 实现精确过期,避免了传统轮询方案的复杂性和潜在错误。测试表明,其性能足以支撑绝大多数生产场景。

源码已可直接用于项目。你只需将 cache.go 和 cache_test.go 放入 timecache 包,即可通过 timecache.NewCache[string](5 * time.Minute) 使用。

欢迎在实际项目中尝试,并根据需求扩展(如添加 Close()、命中率统计等)!

相关推荐
考虑考虑6 小时前
go格式化时间
后端·go
光头闪亮亮1 天前
ZBar 条码/二维码识别工具介绍及golang通过cmd调用ZBar从图片中批量识别二维码
go
东风t西瓜1 天前
golang项目开发环境配置
go
zhuyasen2 天前
在某些 Windows 版本,Go 1.25.x 编译出来的 exe 运行报错:此应用无法在你的电脑上运行
windows·go·编译器
用户89535603282202 天前
深入浅出 Go slices 包:类型安全、内存安全与高性能实践
go
王中阳Go2 天前
Python 的 PyPy 能追上 Go 的性能吗?
后端·python·go
gopyer3 天前
180课时吃透Go语言游戏后端开发11:Go语言中的并发编程
golang·go·游戏开发·并发编程
Java陈序员3 天前
简单好用!一款针对 IT 团队开发的文档管理系统!
mysql·docker·go
程序员爱钓鱼4 天前
Go语言实战案例——进阶与部署篇:性能优化与 pprof 性能分析实践
后端·google·go