高性能 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()、命中率统计等)!

相关推荐
mtngt1118 小时前
AI DDD重构实践
go
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo7 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点8 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理
迷迭香与樱花8 天前
Gin 框架
go·gin
只是懒得想了8 天前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全