必须绕过beego内置memory引擎,改用sync.map(简单场景)或freecache/bigcache(高频大容量),因其支持精确ttl、无gc锁开销;本地ttl应设为redis的1/3~1/2,读取顺序须“先本地后redis”,miss时需用singleflight.group防击穿,并区分redis.nil与真空空值。

本地缓存必须用 memory 引擎,且不能依赖 Beego 内置的 GC 间隔清理
Beego 的 cache.NewCache("memory", ...) 默认使用 interval 参数控制过期扫描频率(如 {"interval":"360"} 表示每 6 分钟扫一次),但这会导致:未被扫描到的过期 key 仍可被 Get 命中,造成逻辑 stale;高并发下大量 goroutine 竞争锁做扫描,反而拖慢性能。
正确做法是绕过 Beego 的 memory 封装,直接用更可控的原生方案:
- 对简单键值、低并发场景:用
sync.Map+ 自带时间戳字段,读时判断time.Since(expiry) > 0 - 对高频、大容量场景:引入
freecache或bigcache,它们自带 LRU 和精确 TTL,无 GC 锁开销 - 所有写入必须同步完成:
Put后立即能被后续Get读到,不能 defer 或异步
Redis 缓存层必须和本地缓存解耦,配置项不能混用 key 字段
Beego cache 模块在初始化 Redis 时,配置里有个 "key" 字段(如 {"key":"beego_cache"}),它**不是缓存 key 的前缀**,而是用于内部拼接连接池标识的冗余字段——实际存入 Redis 的 key 完全由你代码中传给 Put/Get 的第一个参数决定。
常见错误是误以为设置了 "key":"user",就能自动把 Put("1001", ...) 存成 user:1001,结果发现 Redis 里全是裸 key,导致和其他服务冲突或无法统一管理。
解决方式很直接:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 本地缓存 key 格式:保持简洁,如
"user_1001" - Redis 缓存 key 格式:显式加业务前缀,如
"beego:user:1001",由业务代码拼接,不依赖配置项 - 避免在 Beego 的
redis配置里填任何影响 key 生成的字段,"key"字段留空或固定为服务名即可
二级缓存读取顺序必须是 “先本地 → 后 Redis”,且 miss 后需区分 redis.Nil 和真实空值
Beego 的 cache 接口不暴露底层 error 类型,直接调 Get 拿不到 redis.Nil。若你封装了 Redis 层,务必在查 Redis 后显式判断是否为 redis.Nil:
- 如果是
redis.Nil:说明 Redis 里真没这个 key,可能是缓存穿透,应缓存一个短 TTL 的空标记(如 60 秒),防止反复打 DB - 如果 Redis 返回了空字符串或
nullJSON:说明业务上该数据确实不存在(“真空”),此时不应写本地缓存,否则下次读会直接返回空,掩盖了“未命中”状态 - 本地缓存 TTL 应设为 Redis TTL 的 1/3~1/2(如 Redis 是 30 分钟,本地设 10 分钟),避免本地长期 stale
并发场景下必须用 singleflight.Group 拦住重复回源查询
当本地和 Redis 同时 miss,多个 goroutine 可能同时触发 DB 查询,尤其在秒杀、首页加载等场景下极易压垮数据库。Beego 自身不提供防击穿机制,得自己补。
关键点就一条:所有回源加载逻辑(即查 DB 那一步)必须包裹在 singleflight.Group.Do 里,key 必须和缓存 key 一致:
v, err, _ := g.Do("user_1001", func() (interface{}, error) { return queryDB("1001") })- 注意:不要把
Do放在本地缓存LoadOrStore的加载函数里,否则sync.Map的原子性会被破坏;应在缓存 miss 后、查 Redis 前就进入Do -
singleflight的 key 要能唯一标识请求,不能带时间戳或随机数,否则起不到合并效果
本地缓存写入时机容易被忽略:只有 singleflight 返回成功后,才同步写本地 + 异步写 Redis。漏掉这步,就等于白加了一层缓存。










