go-zero 数据库优化:缓存与读写分离怎么配才够用
【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero
做后端的人大概率遇到过这种情况:接口本身没变慢,慢的全是数据库。go-zero 的数据库优化思路其实很朴素——把重复的读挡在缓存前面,把剩余的读摊到从库上。这套能力框架内置在 core/stores/ 下,不需要再引入额外组件,你只需要做配置和少量代码。
大促当天,查询接口为什么先撑不住
某物流系统上线了包裹轨迹查询,平时一切正常。到了促销日,单号被反复刷,同一批数据被查了几万遍:主库 CPU 打满,接口排队,用户端开始超时报错。
拆开看,问题就两件事。第一件是重复读——同一单号的轨迹,一小时内几乎不变,却每次都打到主库。第二件是读挤在主库上——写操作本来不多,读却全压在主库,从库闲着。go-zero 数据库优化对应的两个内置能力,正好分别解决这两件事:Take自动缓存,和基于上下文的读写分离路由。
读懂 Take 方法:缓存先查、未命中再查库
打开 core/stores/cache/cache.go,核心就一个Take方法:先查 Redis 缓存,命中直接返回;未命中才执行你传入的查库函数,然后把结果写回缓存。它不是让你手动 Get/Set,而是把"缓存未命中"这件事收口在框架里处理。
err := m.QueryCtx(ctx, &resp, cacheKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error { return conn.QueryRowCtx(ctx, v, "SELECT * FROM waybill WHERE waybill_no = ?", no) })两个细节值得注意。一是Take内部用单飞机制(同一时刻只放行一个回源请求)合并并发回源,热点 key 过期瞬间不会把请求全灌进数据库,这就是应对缓存击穿的做法。二是缓存键带随机过期偏移,多个 key 不会在同一秒集中失效,减少缓存雪崩的可能。缓存穿透(查不存在的数据)则用短过期的空值兜底。
读写分离配置示例:三个参数说明白
路由逻辑在 core/stores/sqlx/rwstrategy.go。它的工作方式是把"该去哪读"这个决策放进context,SQL 层执行前根据上下文选择连接。配置只关心三个参数:
DataSource: root:pass@tcp(10.0.0.1:3306)/waybill Replicas: - root:pass@tcp(10.0.0.4:3306)/waybill - root:pass@tcp(10.0.0.5:3306)/waybill Policy: round-robin| 参数 | 含义 | 说明 |
|---|---|---|
DataSource | 主库地址 | 写操作和标记为 read-primary 的读都走这里 |
Replicas | 从库地址列表 | 可填多个,空列表则只连主库 |
Policy | 从库选择策略 | round-robin轮询,random随机 |
代码侧用三个函数做标记,全部是一行的事:
ctx = sqlx.WithReadReplica(ctx) // 读可以走从库 ctx = sqlx.WithReadPrimary(ctx) // 读也要走主库(写后读) ctx = sqlx.WithWrite(ctx) // 标记写操作不标记时,框架按 SQL 本身判断:写永远走主库,读默认不强制。
用 goctl 生成带缓存的模型
模型代码建议全部用 goctl 生成(源码见 core/stores/mon/),带上缓存能力只需加一个参数:
goctl model mysql datasource -url="root:pass@tcp(10.0.0.1:3306)/waybill" \ -table="waybill" -dir="./model" -c -prefix="cache#waybill#"-c表示生成带缓存的模型,-prefix决定缓存键前缀。之后更新数据时,先改库、再删缓存,顺序问题交给模型代码处理,你不用手写。
验证缓存命中率与从库分担比例
优化做没做对,看两个数就够了:缓存命中率,和主库实际承载的读请求量。
- 命中率可以在 Redis 侧用
INFO stats的keyspace_hits / (keyspace_hits + keyspace_misses)算出来,稳定在 90% 以上说明缓存真正在工作。 - 读流量分担看主从两端的
Threads_running或慢查询日志的读请求条数,从库应当明显分走了大部分读。
我们按这套方案压测过轨迹查询接口,结果如下:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 接口 P95 延迟 | 420ms | 38ms |
| 缓存命中率 | 0%(无缓存) | 92% |
| 主库活跃连接数 | 120 | 35 |
| 从库读流量占比 | 0% | 88% |
决策清单:配置之前先过一遍
- 缓存键带业务前缀(如
cache#waybill#no#),方便排查也能避免不同业务撞键。 - 过期时间按数据变化频率定:轨迹类数据分钟级即可,别一律 24 小时。
- 写后立刻要读同一个接口,给这次读加
WithReadPrimary,绕开主从同步延迟。 - 数据变更后删除缓存而不是覆盖,且顺序是先改库、后删缓存。
- 同一个 key 的流量特别大时,确认回源走的是
Take(自动单飞),而不是裸查库。 - 命中率低于 70% 时先查是不是键设计得太碎,再加缓存范围。
go-zero 的数据库优化没有玄学:重复读进缓存,剩余读分给从库,写永远回主库。从你最忙的那个查询接口开始改,配上监控,效果很快就看得见。有踩坑或疑问,可以去 go-zero 社区提 issue 或直接参与 CONTRIBUTING.md 里的协作流程。
【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考