为什么你的 Go 应用运行缓慢?很可能你忘了用缓存
你用 Go 编写了一个完美的应用。你为 goroutine、通道和快速的编译器感到自豪。但当负载达到 1000 RPS 时,一切开始变得卡顿。数据库崩溃,API 响应需要 5 秒,用户纷纷转向竞争对手。
熟悉这种情况吗?欢迎来到一个没有 Go 缓存 就寸步难行的世界。在本文中,我们将探讨如何将你的龟速应用变成猎豹。不掺水分,包含真实示例和现成的解决方案。
顺便说一下,我叫 Python,这里是 PythonLib。是的,我知道我们在讨论 Go。但好的代码不受语言界限的限制,我们撰写关于所有技术的有用文章。开始吧!
什么是缓存,为什么 Go 需要它?
数据缓存 是将频繁请求的数据保存在快速存储(内存、Redis、文件)中,以避免每次都去访问慢速数据源(数据库、外部 API、文件系统)。
Go 是一种用于高负载系统的语言。但即使是用 Go,如果不使用缓存,也可能写出慢速的应用。想象一下:你每秒从 PostgreSQL 查询国家列表 1000 次。与其对数据库进行 1000 次查询,不如只查询 1 次,其余 999 次从 Go 内存缓存 中提供数据。
结果:响应时间从 50 毫秒降至 0.5 毫秒。相差 100 倍。这不是魔法,这是合理的架构。
何时必须使用缓存?
- 高负载(每秒 1000+ 请求)
- 慢速的外部 API(支付、天气、汇率)
- 繁重的计算(报告、分析)
- 频繁查询的参考数据(城市列表、商品类别)
缓存策略:选择正确的方式
为了避免缓存变成反模式,你需要选择正确的 缓存策略。以下是在 Go 中有效的主要方案:
1. Cache-Aside(惰性缓存)
最流行的模式。应用首先检查缓存。如果没有数据(缓存未命中),则访问数据库,将结果保存到缓存,然后返回给客户端。
func GetUser(cache *redis.Client, db *sql.DB, userID int) (*User, error) { // 尝试从缓存获取 val, err := cache.Get(ctx, "user:"+strconv.Itoa(userID)).Result() if err == nil { user := &User{} json.Unmarshal([]byte(val), user) return user, nil }
// 缓存未命中——访问数据库 user, err := db.Query("SELECT * FROM users WHERE id = ?", userID) if err != nil { return nil, err }
// 保存到缓存,有效期 1 小时 data, _ := json.Marshal(user) cache.Set(ctx, "user:"+strconv.Itoa(userID), data, time.Hour)
return user, nil}2. Write-Through(穿透写入)
数据同时写入缓存和数据库。优点:缓存始终是最新的。缺点:写入稍慢。
3. TTL(生存时间)
最简单的 缓存失效 方法。数据存活固定时间,之后自动删除。
// 缓存 5 分钟cache.Set(ctx, "key", value, 5*time.Minute)在 Go 中手动实现内存缓存
有时你不想仅仅为了几个键而启动 Redis。对于这种情况,Go 内存缓存 非常合适。让我们编写一个简单但可用的版本:
package cache
import ( "sync" "time")
type Item struct { Value interface{} Expiration int64}
type Cache struct { items map[string]Item mu sync.RWMutex}
func New() *Cache { c := &Cache{ items: make(map[string]Item), } // 启动 goroutine 清理过期条目 go c.cleanup() return c}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Un