缓存:让应用飞起来
缓存是提升系统性能最有效的手段之一。当你的应用频繁访问数据库、调用远程 API 或执行复杂的计算时,缓存可以显著减少响应时间和系统负载。
Go 提供了多种缓存方案:从简单的内存缓存到强大的分布式缓存(如 Redis)。今天我们就来学习如何在 Go 中实现和使用缓存。
为什么需要缓存?
考虑这样一个场景:你的电商网站有一个热门商品详情页,每秒有 1000 次请求。每次都查询数据库,数据库很快就扛不住了。
但如果把商品详情缓存起来,第一次查询数据库,后续请求直接读缓存,数据库压力就降低到原来的 1/1000。
缓存的优势:
- 降低延迟:内存访问比磁盘/网络快几个数量级
- 减少负载:减轻数据库和下游服务的压力
- 提升吞吐:同样的资源可以处理更多请求
但缓存也有代价:
- 数据一致性:缓存和源数据可能不一致
- 内存占用:缓存需要占用内存
- 复杂度:需要处理缓存失效、更新等逻辑
内存缓存
简单的 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 的缓存实现:
- 内存缓存:简单 Map、自动清理、LRU
- 第三方库:go-cache、bigcache
- Redis:分布式缓存
- 缓存模式:Cache-Aside、Singleflight
- 失效策略:TTL、主动失效、版本号
- 多级缓存:本地 + 远程
缓存是提升性能的重要手段,但也要谨慎使用。记住:缓存是数据库的延伸,不是替代品。
练习时间
- LFU 缓存:实现按访问频率淘汰的缓存
- 分布式锁:用 Redis 实现分布式锁
- 缓存预热:实现应用启动时预加载热点数据
- 缓存监控:统计缓存命中率、响应时间等指标
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。