不能直接用 sync.map 做多级缓存,因为它是单进程内线程安全哈希表,无过期、驱逐、穿透保护,也无法感知 redis 等远程层状态变化;多节点下各实例数据完全隔离,强行当 l1 用需手动维护跨进程一致性,出问题难以定位。

为什么不能直接用 sync.Map 做多级缓存同步更新
因为 sync.Map 是单进程内线程安全的哈希表,不带过期、驱逐、穿透保护,更无法感知远程层(如 Redis)状态变化。你写入本地后,其他节点完全不知道——这不是 bug,是设计如此。强行把它当 L1 用,等于手动维护跨进程一致性,出问题时连日志都难定位。
本地缓存失效必须靠外部信号驱动
Go 的本地缓存天然不同步,多节点下唯一可行路径是让所有实例监听同一份失效事件。推荐用 Redis 的 key 过期事件(__keyevent@0__:expired),但要注意:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- Redis 默认关闭 key event,需在
redis.conf中显式启用notify-keyspace-events Ex - 哨兵 key 必须原子设置:
SET cache:invalid:user:123 "1" EX 60,别拆成SET+EXPIRE,否则有竞态 - 低版本 Redis(__keyspace@0__:* 再字符串匹配,CPU 开销略增
- 监听到事件后只做
l1.Delete("user:123"),别在回调里查 DB 或触发重载——那会阻塞事件循环
写操作必须区分 Set 和 Delete 的传播策略
多级缓存的写一致性比读更难,不能默认“所有层一起写”。关键在于按语义选策略:
-
Set:优先写 Redis,成功后再异步刷新本地(避免本地写失败导致后续读 miss 加载旧值);若 Redis 不可用,可降级为只写本地并标记dirty=true,等恢复后补偿 -
Delete:必须同步清除所有层级——l1.Remove(key)+redis.Del(ctx, key),否则下次Get会从本地返回已删除数据 - 别用
SET覆盖代替DEL:万一 DB 写成功、缓存SET失败,旧值就卡死了 - 关联数据(如用户资料变更后要清订单缓存)要用前缀扫描或布隆过滤器识别待删范围,别漏删
并发穿透和双删失败必须有兜底机制
两个 goroutine 同时发现缓存 miss,都去查 DB 并写缓存,不仅浪费资源,还可能因写入时序导致不一致。核心解法是:
- 用
singleflight.Group拦住重复加载:传入唯一 key(建议加前缀如"load:user:123"),loader 函数内部必须处理ctx超时 -
Delete失败不能回滚 DB:DB 已改成功,缓存只是辅助层,只需记日志(如log.Warn("failed to delete cache key: user:123", "err", err)) - 空值也要缓存防穿透:设短 TTL(如
time.Minute),但只缓存业务语义上的“不存在”,不缓存sql.ErrNoRows等错误 - 高频空 key 场景叠加布隆过滤器,在缓存层前置拦截,减少无效 DB 查询
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










