☰
go-zero 数据库性能优化指南:缓存与读写分离该什么时候上
2026/10/18 14:29:19 网站建设 项目流程

go-zero 数据库性能优化指南:缓存与读写分离该什么时候上

【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero

go-zero 项目做数据库性能优化,最常见的卡点就两个:接口读得太频繁、每次都白跑一遍 SQL;或者一个主库同时扛下了读和写。框架把这两类问题的解法都内置好了——一层可配置的 Redis 缓存,和一套读写分离的路由。这篇文章不复述缓存的概念,只回答三件事:什么时候该动手、先动哪一步、动手时框架留了哪些开关。

先分清:是读太多,还是写太贵

优化之前先做判断,否则容易白上加缓存。看两个信号就够了:

  • 读多写少、数据可短暂不一致(比如详情页、配置项、榜单)——典型该上缓存的形态。特征是同一批 key 被反复查,SQL 本身不慢,慢在量。
  • 单主库 CPU 或连接数接近上限、读和写混在一起,且线上已经有从库——这才是读写分离要解决的问题。特征是慢在并发,而不是单条 SQL。

两者不冲突,但出手顺序不同:先压读,再摊读。

加缓存的时机:读多写少且能容忍短暂不一致

判断条件满足后,缓存层的模型不建议手写。用 goctl 生成时带上-c打开缓存,生成的方法会先查缓存、未命中再回源数据库并回填,前缀用-prefix控制,避免不同模块的 key 撞车。

# -c 打开缓存 goctl model mysql ddl -src ./sql/user.sql -dir ./model -c # 按数据源生成,-prefix 指定缓存 key 前缀(默认 cache) goctl model mysql datasource \ -url="root:pass@tcp(127.0.0.1:3306)/demo" -table="user" -c -prefix="user"

生成的FindOne内部走的是TakeCtx:命中直接返回,未命中执行查询函数并回填,签名就是func(val any) error,所以回源逻辑写得很干净。

var resp User key := fmt.Sprintf("user#id#%d", id) err := m.c.TakeCtx(ctx, &resp, key, func(v any) error { q := fmt.Sprintf("select %s from user where id = ? limit 1", userRows) return m.conn.QueryRowCtx(ctx, v, q, id) })

多节点时用一致性哈希把不同 key 分散到不同缓存节点,配置里给每个节点带Weight:

Cache: - Host: redis1:6379 Type: node Weight: 100 - Host: redis2:6379 Type: node Weight: 100

三种缓存故障,go-zero 已经内置了防御

穿透、击穿、雪崩不用自己再包一层。看 core/stores/cache/ 里的实现就能对应上:

  • 穿透(查不存在的 key 一直打库):回源发现没有时,写入一个*占位符并挂短过期时间notFoundExpiry,挡掉后续相同查询。
  • 击穿(热点 key 过期瞬间被打穿):取数走barrier syncx.SingleFlight,同一 key 的并发请求只放一个回源,其余等结果。
  • 雪崩(大量 key 同时过期):过期时间不是定值,而是落在基础值[0.95, 1.05]的抖动区间里,错开失效时刻。

这些是默认行为,通常不用改;真要调,就是调过期时间和占位符时长。

读写分离的时机与 go-zero 读写分离配置

只有当单主库扛不住读、并且已经有从库时,再上读写分离。配置比想象中直接:一个DataSource指主库,Replicas列出从库,Policy选策略,见 core/stores/sqlx/。

DataSource: root:pass@tcp(master:3306)/demo Replicas: - root:pass@tcp(slave1:3306)/demo - root:pass@tcp(slave2:3306)/demo Policy: round-robin # 或 random

两种读库策略:轮询与随机

  • round-robin(默认):用原子计数按顺序取,从库间流量均匀,适合读压力稳定的场景。
  • random:随机取一个从库,实现更轻,适合对单库倾斜不敏感的场景。

只有配置了多个从库时策略才真正生效;单从库时策略不影响结果。

用 Context 决定这次读走主库还是从库

路由是隐式的:默认和写都走主库,只有显式指定读从库才走从库。三个开关都在sqlx包:

// 写后立即读,强制走主库,规避主从延迟读到旧值 ctx := sqlx.WithReadPrimary(context.Background()) user, _ := usermodel.FindOne(ctx, 1) // 普通读,走从库,摊薄主库压力 ctx = sqlx.WithReadReplica(context.Background()) users, _ := usermodel.FindAll(ctx)

规则很朴素:对新鲜度敏感的读(下单、改完即看)用WithReadPrimary,其余批量读、列表读交给从库。

缓存和读写分离叠在一起时的一致性取舍

两层同时开,等于引入了两个可能读到旧值的地方,要分开处理:

  • 写后清缓存:更新走主库成功后,主动删掉对应 key,让下次读重新回源。
if _, err := usermodel.Update(ctx, u); err != nil { return err } key := fmt.Sprintf("user#id#%d", u.Id) return cache.DelCtx(ctx, key)
  • 写后立刻读走主库:清缓存解决的是"下次",解决不了"这一次"读到从库旧值的问题,所以在同一请求内回读时用WithReadPrimary。

取舍点在于业务能接受多旧:能容忍秒级延迟的走从库,容忍不了的读主库。别为了省压力把强一致读也丢给从库。

动手顺序:一份检查清单

  1. 先给读多写少、可容忍短暂不一致的接口挂缓存,用 goctl 生成,别手写。
  2. 观察主库指标,读压力仍顶到上限、且有从库时,再上读写分离并配好策略。
  3. 最后补一致性:写后清缓存、关键读走主库。

顺序反了容易白做——主库还没撑不住就上从库,或数据强一致却把读丢给从库。先压读,再摊读,最后兜住一致性,就是这套方案最省事的推进方式。

【免费下载链接】go-zeroA cloud-native Go microservices framework with cli tool for productivity.项目地址: https://gitcode.com/GitHub_Trending/go/go-zero

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询