缓存:让应用飞起来

学习 Go 的缓存实现:从内存缓存到分布式缓存,掌握缓存的设计模式和最佳实践

缓存:让应用飞起来

缓存是提升系统性能最有效的手段之一。当你的应用频繁访问数据库、调用远程 API 或执行复杂的计算时,缓存可以显著减少响应时间和系统负载。

Go 提供了多种缓存方案:从简单的内存缓存到强大的分布式缓存(如 Redis)。今天我们就来学习如何在 Go 中实现和使用缓存。

为什么需要缓存?

考虑这样一个场景:你的电商网站有一个热门商品详情页,每秒有 1000 次请求。每次都查询数据库,数据库很快就扛不住了。

但如果把商品详情缓存起来,第一次查询数据库,后续请求直接读缓存,数据库压力就降低到原来的 1/1000。

缓存的优势:

  1. 降低延迟:内存访问比磁盘/网络快几个数量级
  2. 减少负载:减轻数据库和下游服务的压力
  3. 提升吞吐:同样的资源可以处理更多请求

但缓存也有代价:

  1. 数据一致性:缓存和源数据可能不一致
  2. 内存占用:缓存需要占用内存
  3. 复杂度:需要处理缓存失效、更新等逻辑

内存缓存

简单的 Map 缓存

最简单的缓存就是一个 map

package main

import (
	"fmt"
	"sync"
	"time"
)

type CacheItem struct {
	Value     interface{}
	ExpiresAt time.Time
}

type SimpleCache struct {
	mu    sync.RWMutex
	items map[string]CacheItem
}

func NewSimpleCache() *SimpleCache {
	return &SimpleCache{
		items: make(map[string]CacheItem),
	}
}

func (c *SimpleCache) Set(key string, value interface{}, ttl time.Duration) {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	c.items[key] = CacheItem{
		Value:     value,
		ExpiresAt: time.Now().Add(ttl),
	}
}

func (c *SimpleCache) Get(key string) (interface{}, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()
	
	item, ok := c.items[key]
	if !ok {
		return nil, false
	}
	
	// 检查是否过期
	if time.Now().After(item.ExpiresAt) {
		return nil, false
	}
	
	return item.Value, true
}

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

func main() {
	cache := NewSimpleCache()
	
	cache.Set("user:123", "张三", 5*time.Minute)
	
	if value, ok := cache.Get("user:123"); ok {
		fmt.Println("缓存命中:", value)
	} else {
		fmt.Println("缓存未命中")
	}
}

⚠️ 问题:这个缓存有过期时间,但过期的项不会自动删除,会一直占用内存。

自动清理过期项

type SimpleCache struct {
	mu    sync.RWMutex
	items map[string]CacheItem
}

func NewSimpleCache() *SimpleCache {
	c := &SimpleCache{
		items: make(map[string]CacheItem),
	}
	
	// 启动后台清理 goroutine
	go c.cleanup()
	
	return c
}

func (c *SimpleCache) cleanup() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()
	
	for range ticker.C {
		c.mu.Lock()
		now := time.Now()
		for key, item := range c.items {
			if now.After(item.ExpiresAt) {
				delete(c.items, key)
			}
		}
		c.mu.Unlock()
	}
}

LRU 缓存

LRU(Least Recently Used,最近最少使用)是一种常见的缓存淘汰策略:当缓存满了时,删除最久未使用的项。

package main

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

type LRUCache struct {
	capacity int
	mu       sync.Mutex
	items    map[string]*list.Element
	order    *list.List
}

type entry struct {
	key   string
	value interface{}
}

func NewLRUCache(capacity int) *LRUCache {
	return &LRUCache{
		capacity: capacity,
		items:    make(map[string]*list.Element),
		order:    list.New(),
	}
}

func (c *LRUCache) Get(key string) (interface{}, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	if elem, ok := c.items[key]; ok {
		// 移动到链表头部(最近使用)
		c.order.MoveToFront(elem)
		return elem.Value.(*entry).value, true
	}
	
	return nil, false
}

func (c *LRUCache) Set(key string, value interface{}) {
	c.mu.Lock()
	defer c.mu.Unlock()
	
	// 如果已存在,更新值并移动到头部
	if elem, ok := c.items[key]; ok {
		c.order.MoveToFront(elem)
		elem.Value.(*entry).value = value
		return
	}
	
	// 如果缓存满了,删除最久未使用的(链表尾部)
	if c.order.Len() >= c.capacity {
		oldest := c.order.Back()
		if oldest != nil {
			c.order.Remove(oldest)
			delete(c.items, oldest.Value.(*entry).key)
		}
	}
	
	// 添加新项到头部
	elem := c.order.PushFront(&entry{key: key, value: value})
	c.items[key] = elem
}

func main() {
	cache := NewLRUCache(3)
	
	cache.Set("a", 1)
	cache.Set("b", 2)
	cache.Set("c", 3)
	
	fmt.Println(cache.Get("a"))  // 1, true
	
	cache.Set("d", 4)  // 缓存满了,删除最久未使用的 "b"
	
	fmt.Println(cache.Get("b"))  // nil, false
	fmt.Println(cache.Get("c"))  // 3, true
	fmt.Println(cache.Get("d"))  // 4, true
}

使用第三方库

go-cache

go-cache 是一个功能完善的内存缓存库:

package main

import (
	"fmt"
	"time"
	
	"github.com/patrickmn/go-cache"
)

func main() {
	// 创建缓存,默认过期时间 5 分钟,每 10 分钟清理一次
	c := cache.New(5*time.Minute, 10*time.Minute)
	
	// 设置
	c.Set("foo", "bar", cache.DefaultExpiration)
	c.Set("baz", 42, 10*time.Minute)
	c.Set("temp", "data", 30*time.Second)
	
	// 获取
	if foo, found := c.Get("foo"); found {
		fmt.Println("foo:", foo)
	}
	
	// 获取并类型断言
	if baz, found := c.Get("baz"); found {
		fmt.Println("baz:", baz.(int))
	}
	
	// 删除
	c.Delete("temp")
	
	// 清空
	c.Flush()
	
	// 获取或加载
	value, found := c.Get("key")
	if !found {
		// 从数据库加载
		value = loadFromDB("key")
		c.Set("key", value, cache.DefaultExpiration)
	}
	
	// 增加/减少(仅数值)
	c.Set("counter", 0, cache.DefaultExpiration)
	c.Increment("counter", 1)
	c.Decrement("counter", 1)
}

bigcache

bigcache 是一个高性能的大容量缓存,适合缓存大量数据:

package main

import (
	"fmt"
	"log"
	"time"
	
	"github.com/allegro/bigcache/v3"
)

func main() {
	config := bigcache.Config{
		Shards:             1024,
		LifeWindow:         10 * time.Minute,
		CleanWindow:        5 * time.Minute,
		MaxEntriesInWindow: 1000 * 10 * 60,
		MaxEntrySize:       500,
		Verbose:            true,
		HardMaxCacheSize:   8192,  // MB
	}
	
	cache, err := bigcache.New(context.Background(), config)
	if err != nil {
		log.Fatal(err)
	}
	
	// 设置(值是 []byte)
	cache.Set("key", []byte("value"))
	
	// 获取
	entry, err := cache.Get("key")
	if err != nil {
		log.Println("未找到:", err)
	} else {
		fmt.Println("值:", string(entry))
	}
	
	// 删除
	cache.Delete("key")
}

Redis 缓存

Redis 是最流行的分布式缓存。在 Go 中使用 go-redis

package main

import (
	"context"
	"fmt"
	"log"
	"time"
	
	"github.com/go-redis/redis/v8"
)

var ctx = context.Background()

func main() {
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})
	
	// 测试连接
	if err := rdb.Ping(ctx).Err(); err != nil {
		log.Fatal("连接 Redis 失败:", err)
	}
	
	// 设置(带过期时间)
	err := rdb.Set(ctx, "user:123", "张三", 10*time.Minute).Err()
	if err != nil {
		log.Fatal(err)
	}
	
	// 获取
	val, err := rdb.Get(ctx, "user:123").Result()
	if err == redis.Nil {
		fmt.Println("key 不存在")
	} else if err != nil {
		log.Fatal(err)
	} else {
		fmt.Println("user:123:", val)
	}
	
	// 哈希
	rdb.HSet(ctx, "user:456", map[string]interface{}{
		"name":  "李四",
		"email": "lisi@example.com",
		"age":   30,
	})
	rdb.Expire(ctx, "user:456", 10*time.Minute)
	
	// 获取哈希字段
	name, _ := rdb.HGet(ctx, "user:456", "name").Result()
	fmt.Println("name:", name)
	
	// 获取所有字段
	user, _ := rdb.HGetAll(ctx, "user:456").Result()
	fmt.Println("user:", user)
}

缓存模式

Cache-Aside(旁路缓存)

最常用的缓存模式:

func GetUser(id int) (*User, error) {
	key := fmt.Sprintf("user:%d", id)
	
	// 1. 先查缓存
	cached, err := redis.Get(ctx, key).Result()
	if err == nil {
		// 缓存命中
		var user User
		json.Unmarshal([]byte(cached), &user)
		return &user, nil
	}
	
	// 2. 缓存未命中,查数据库
	user, err := db.GetUser(id)
	if err != nil {
		return nil, err
	}
	
	// 3. 写入缓存
	data, _ := json.Marshal(user)
	redis.Set(ctx, key, data, 10*time.Minute)
	
	return user, nil
}

Cache Stampede(缓存雪崩)

当大量请求同时访问一个不存在的缓存项时,会导致大量并发数据库查询。解决方案是使用 singleflight

package main

import (
	"sync"
	
	"golang.org/x/sync/singleflight"
)

var sf singleflight.Group

func GetUserWithSingleflight(id int) (*User, error) {
	key := fmt.Sprintf("user:%d", id)
	
	// 先查缓存
	if cached := getFromCache(key); cached != nil {
		return cached, nil
	}
	
	// 使用 singleflight 确保同一时间只有一个请求去查数据库
	result, err, _ := sf.Do(key, func() (interface{}, error) {
		// 再查一次缓存(可能其他 goroutine 已经加载了)
		if cached := getFromCache(key); cached != nil {
			return cached, nil
		}
		
		// 查数据库
		user, err := db.GetUser(id)
		if err != nil {
			return nil, err
		}
		
		// 写缓存
		setCache(key, user, 10*time.Minute)
		return user, nil
	})
	
	if err != nil {
		return nil, err
	}
	
	return result.(*User), nil
}

缓存失效策略

1. TTL(过期时间)

// 根据数据变化频率设置不同的 TTL
const (
	UserTTL    = 30 * time.Minute  // 用户信息变化少
	ProductTTL = 5 * time.Minute   // 商品信息变化适中
	StockTTL   = 30 * time.Second  // 库存变化频繁
)

2. 主动失效

func UpdateUser(user *User) error {
	// 更新数据库
	if err := db.UpdateUser(user); err != nil {
		return err
	}
	
	// 删除缓存
	key := fmt.Sprintf("user:%d", user.ID)
	redis.Del(ctx, key)
	
	return nil
}

3. 版本号

type CachedUser struct {
	Version int
	User    *User
}

// 获取时检查版本
func GetUser(id int) (*User, error) {
	key := fmt.Sprintf("user:%d", id)
	versionKey := fmt.Sprintf("user:%d:version", id)
	
	// 获取当前版本
	currentVersion, _ := redis.Get(ctx, versionKey).Int()
	
	// 获取缓存
	var cached CachedUser
	data, _ := redis.Get(ctx, key).Bytes()
	json.Unmarshal(data, &cached)
	
	if cached.Version == currentVersion {
		return cached.User, nil
	}
	
	// 版本不匹配,重新加载
	user, _ := db.GetUser(id)
	cached = CachedUser{Version: currentVersion, User: user}
	data, _ = json.Marshal(cached)
	redis.Set(ctx, key, data, 30*time.Minute)
	
	return user, nil
}

// 更新时增加版本号
func UpdateUser(user *User) error {
	db.UpdateUser(user)
	versionKey := fmt.Sprintf("user:%d:version", user.ID)
	redis.Incr(ctx, versionKey)
	return nil
}

实战:多级缓存

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"time"
	
	"github.com/go-redis/redis/v8"
	"github.com/patrickmn/go-cache"
)

type MultiLevelCache struct {
	local  *cache.Cache
	remote *redis.Client
	ctx    context.Context
}

func NewMultiLevelCache(redisClient *redis.Client) *MultiLevelCache {
	return &MultiLevelCache{
		local:  cache.New(1*time.Minute, 2*time.Minute),
		remote: redisClient,
		ctx:    context.Background(),
	}
}

func (c *MultiLevelCache) Get(key string, value interface{}) error {
	// 1. 先查本地缓存
	if cached, found := c.local.Get(key); found {
		data, _ := json.Marshal(cached)
		json.Unmarshal(data, value)
		return nil
	}
	
	// 2. 查远程缓存
	data, err := c.remote.Get(c.ctx, key).Bytes()
	if err == nil {
		json.Unmarshal(data, value)
		// 回填本地缓存
		c.local.Set(key, value, cache.DefaultExpiration)
		return nil
	}
	
	// 3. 都未命中
	return fmt.Errorf("缓存未命中")
}

func (c *MultiLevelCache) Set(key string, value interface{}, ttl time.Duration) error {
	// 写本地缓存
	c.local.Set(key, value, ttl)
	
	// 写远程缓存
	data, _ := json.Marshal(value)
	return c.remote.Set(c.ctx, key, data, ttl).Err()
}

func (c *MultiLevelCache) Delete(key string) {
	c.local.Delete(key)
	c.remote.Del(c.ctx, key)
}

func main() {
	rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
	cache := NewMultiLevelCache(rdb)
	
	// 设置
	cache.Set("user:123", map[string]interface{}{
		"name": "张三",
		"age":  25,
	}, 10*time.Minute)
	
	// 获取
	var user map[string]interface{}
	if err := cache.Get("user:123", &user); err == nil {
		fmt.Println("用户:", user)
	}
}

小结

今天我们学习了 Go 的缓存实现:

  1. 内存缓存:简单 Map、自动清理、LRU
  2. 第三方库:go-cache、bigcache
  3. Redis:分布式缓存
  4. 缓存模式:Cache-Aside、Singleflight
  5. 失效策略:TTL、主动失效、版本号
  6. 多级缓存:本地 + 远程

缓存是提升性能的重要手段,但也要谨慎使用。记住:缓存是数据库的延伸,不是替代品

练习时间

  1. LFU 缓存:实现按访问频率淘汰的缓存
  2. 分布式锁:用 Redis 实现分布式锁
  3. 缓存预热:实现应用启动时预加载热点数据
  4. 缓存监控:统计缓存命中率、响应时间等指标

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页