go-zero 缓存与读写分离实战:权限查询 P99 从 2s 压到 18ms 📅 发布时间:2026/9/2 9:39:14 👁 浏览次数: go-zero 缓存与读写分离实战权限查询 P99 从 2s 压到 18ms【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero周五 14:40网关权限查询接口的 P99 从 80ms 跳到 2 秒MySQL 活跃连接数打满 500。流量没有涨涨的是同一条鉴权 SQL 的并发量——每个请求都直连主库查 token。问题不在 SQL 本身在 go-zero 的两个内置能力没启用缓存cache与数据库读写分离一主多从。先查缓存再打数据库Take 的机制go-zero 的缓存组件core/stores/cache把先查缓存、未命中回源数据库封装成一次Take调用先问 Redis命中直接返回未命中时只放一个 goroutine 去查库同一 key 的其他并发请求挂在一个叫 SingleFlight单飞合并相同请求的屏障上等同一个结果查完顺手写回 Redis。这样设计的原因只有一个key 过期瞬间1000 个并发只会变成 1 次数据库查询。请求 → Take(key) ├─ Redis 命中 ──────────────→ 返回P99 ~1ms ├─ 空值占位 * ────────────→ 直接 404不打库 └─ 未命中 → SingleFlight 合并 → 仅 1 个 goroutine 查 MySQL → 写回 Redis → 全员返回权限系统最小可跑示例鉴权网关有个高频查询按 token 查权限。把 goctl 生成的模型套上缓存层关键改动就一处——QueryRow换成QueryCtxconst ( cachePrefix cache#perm# // ← 关键TTL 决定权限撤销最长多久生效 expiration 30 * time.Second ) func (m *defaultPermModel) FindOneByToken(ctx context.Context, token string) (*Perm, error) { var resp Perm err : m.QueryCtx(ctx, resp, cachePrefixtoken, func(ctx context.Context, conn sqlx.SqlConn, v any) error { return conn.QueryRowCtx(ctx, v, findOneByToken, token) // ← 关键仅缓存未命中时执行 }) if errors.Is(err, sqlx.ErrNotFound) { return nil, ErrPermNotFound } return resp, err }FindOneByToken是模型里已有的 SQL 常量QueryCtx会先查 Redis未命中才执行回调里的查询并写回。服务启动时加缓存客户端var cacheClient cache.Cache func init() { cacheClient cache.New(c.CacheConf, syncx.DefaultSingleFlight, cache.NewStat(), sqlx.ErrNotFound, cache.WithExpiry(30*time.Second), // ← 关键 cache.WithNotFoundExpiry(15*time.Second)) }读写分离在数据库配置里声明一个字段的事DataSource: user:passtcp(127.0.0.1:3306)/perm Replicas: [user:passtcp(127.0.0.1:3307)/perm] # ← 关键从库 Policy: round-robin # ← 关键轮询从库生产参数怎么选六项配置对照配置位置参数默认值推荐取值什么时候用cache 包Expiry7d30s–5m业务数据的可容忍陈旧窗口权限类数据取短cache 包NotFoundExpiry1m15s防不存在 key 反复打库太短会让新 token 短暂查不到缓存节点过期随机偏移±5%保持默认打散同时到期点防集中回源sqlxReplicas无1–3 台从库读流量超过主库 30% 时挂从库sqlxPolicyround-robin节点均衡选轮询从库规格不一、存在快慢节点时选 randomctxWithReadPrimary / WithReadReplica未指定按语义选写后读用主库列表页用从库WithReadPrimary、WithReadReplica、WithWrite在 core/stores/sqlx/rwstrategy.go 里通过 context 传递不指定时写操作永远走主库。用指标确认方案真的生效生效看三个数缓存命中率Prometheus 的 cache 指标按 hit / miss 打点(hit)/(hitmiss)应稳定在 85% 以上。权限 token 这种重复查询上线后实测从 0% 提到 92%。数据库 QPShttp_database_queries的速率降到原来的 1/5 左右。日志关键字出现unmarshal cache说明缓存值反序列化失败、条目已被自动删除属于自愈行为但频繁出现就该查序列化兼容性。出现命中率涨了数据库 QPS 没降的反模式说明流量绕过了Take走了裸Get去查调用链。调参方向命中率低于 70% 且回源 QPS 高把 Expiry 往上加一档撤销 token 后生效太慢缩短 Expiry而不是加长 NotFoundExpiry。主从延迟超过 500ms 时该方案要降级从库复制延迟超过 500ms 时round-robin 轮询会把写后立即读的请求打到陈旧从库上鉴权场景里就是刚撤销的 token 还能用几百毫秒。这类请求必须sqlx.WithReadPrimary(ctx)强制读主库从库只承接列表、审计这类弱一致读。Redis 故障时Take快速失败、不放行流量到数据库源码注释原话不让灾难穿透到 DB。数据库保住了但依赖缓存的读路径全挂核心鉴权链路要准备降级开关直接查主库。TTL 是两头的矛盾设短了命中率掉到 70% 以下、数据库 QPS 反弹设长了权限变更的生效窗口拉长。平衡点靠指标定不靠感觉定。单飞还有个小副作用同一 key 的并发请求共享首次查库结果对权限这种低频变更的数据是安全的对秒级变更的数据要想清楚。Take加主从配置就是 go-zero 压住数据库的两个内置能力剩下要做的只有一件事——盯着命中率把 TTL 调到对的位置。【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考