高性能 Go 语言带 TTL 的内存缓存实现:精确过期、自动刷新、并发安全
在高并发服务开发中,内存缓存是提升性能的关键组件。而支持自动过期(TTL) 的缓存更是常见需求。本文将介绍一种基于 Go 语言实现的高性能、并发安全、支持过期时间自动刷新的内存缓存方案,并附完整源码、测试用例及性能分析。
🎯 设计目标
- 所有条目使用统一的全局 TTL(Time-To-Live)
- Set(key, value) 会重置该 key 的过期时间
- 过期后自动删除,无需手动清理
- 并发安全:支持多 goroutine 同时读写
- 高性能:读操作零分配,写操作开销可控
- 精确过期:避免"提前误删"或"延迟删除"
🧠 核心思路:为每个 Key 绑定独立 Timer
传统方案常使用后台协程轮询 + 堆(heap)管理过期时间,但存在两个问题:
- 同一个 key 多次 Set 会导致堆中冗余项;
- 旧的过期计划可能误删刚被刷新的 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()、命中率统计等)!