数据库与缓存
go-zero 在数据访问层提供 sqlx(数据库连接封装)和 sqlc(带 Redis 缓存的数据库连接),通过 goctl model 一键生成 CRUD 代码和缓存管理逻辑。
架构层次
┌─────────────────────────────┐
│ Logic 层(业务逻辑) │
│ 调用 Repository 封装的方法 │
├─────────────────────────────┤
│ Model 层(goctl 生成) │
│ 单表 CRUD + 自动缓存管理 │
├─────────────────────────────┤
│ sqlc / sqlx │
│ 数据库连接 + Redis 缓存 │
├─────────────────────────────┤
│ MySQL / Redis │
└─────────────────────────────┘sqlx:数据库连接
初始化
import "github.com/zeromicro/go-zero/core/stores/sqlx"
// ServiceContext 中初始化
type ServiceContext struct {
Config config.Config
DB sqlx.SqlConn // 纯数据库连接,无缓存
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
DB: sqlx.NewMysql(c.DataSource),
}
}配置
DataSource: root:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=true使用示例
// 查询单条
var user User
err := s.DB.QueryRowCtx(ctx, &user, "SELECT * FROM user WHERE id = ?", id)
// 查询多条
var users []User
err := s.DB.QueryRowsCtx(ctx, &users, "SELECT * FROM user WHERE status = ?", status)
// 执行写操作
result, err := s.DB.ExecCtx(ctx, "UPDATE user SET name = ? WHERE id = ?", name, id)
// 事务
err := s.DB.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
if _, err := session.ExecCtx(ctx, "INSERT INTO ...", ...); err != nil {
return err
}
if _, err := session.ExecCtx(ctx, "UPDATE ...", ...); err != nil {
return err
}
return nil
})sqlc:带缓存的数据库访问(核心)
初始化
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(
sqlx.NewMysql(c.DataSource),
c.Cache,
),
}
}配置
DataSource: root:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=true
Cache:
- Host: 127.0.0.1:6379
Pass: ""
Type: node生成 Model
# 从 DDL 文件生成
goctl model mysql ddl -c -src user.sql -dir .
# 从数据库直接生成
goctl model mysql datasource \
-url "root:password@tcp(127.0.0.1:3306)/dbname" \
-table "user" \
-c \
-dir ./model生成的文件
model/
├── usermodel_gen.go # 自动生成:基础 CRUD + 缓存
├── usermodel.go # 自定义扩展:复杂查询、批量操作
└── vars.go # 常量定义💡
goctl再次生成时只覆盖usermodel_gen.go,你的usermodel.go不受影响。
sqlc 缓存机制(重要)
读操作:Cache Aside 模式
FindOne(ctx, id)
→ 查 Redis 缓存 (key: cache:user:id:123)
→ 命中 → 直接返回
→ 未命中 → 查 MySQL
→ 回填 Redis 缓存
→ 返回写操作:删除缓存
Insert(ctx, data)
→ 写入 MySQL
→ 删除 Redis 缓存 (key: cache:user:id:123)
Update(ctx, data)
→ 更新 MySQL
→ 删除 Redis 缓存
Delete(ctx, id)
→ 删除 MySQL
→ 删除 Redis 缓存🔬 为什么删缓存而不是更新缓存? 更新缓存存在并发写覆盖问题:A 更新 DB → B 更新 DB → B 更新缓存 → A 更新缓存(旧数据覆盖了新数据)。删除缓存是最安全的做法。
索引缓存
对于按唯一索引查询,sqlc 提供两层缓存:
FindOneByEmail(ctx, email)
→ 查 Redis: cache:user:email:alice@example.com → 拿到 id=123
→ 查 Redis: cache:user:id:123 → 拿到完整数据
→ 返回🚨 sqlc 仅支持主键和唯一索引的单条记录缓存,不支持批量查询缓存。批量查询请用
sqlx.SqlConn直接查 DB。
缓存配置选项
过期时间
// 自定义缓存过期时间(默认 7 天)
model.NewUserModel(conn, c, model.WithExpiry(time.Hour))
// 空值缓存过期时间(防缓存穿透,默认 1 分钟)
model.NewUserModel(conn, c, model.WithNotFoundExpiry(time.Second*5))不读缓存/不删缓存
// 按主键查询——绕过缓存直接查库
var user User
err := db.QueryRowNoCacheCtx(ctx, &user, "SELECT * FROM user WHERE id = ?", id)
// 按索引查询——绕过缓存
err := db.QueryRowNoCacheCtx(ctx, &user, "SELECT * FROM user WHERE email = ?", email)
// 执行写入——不清除缓存(慎用)
result, err := db.ExecNoCacheCtx(ctx, "DELETE FROM user WHERE id = ?", id)缓存一致性原理与取舍
读写并发问题
时间线:
T1: 线程A 删除缓存(准备更新DB)
T2: 线程B 读缓存(miss)→ 读DB(旧值)→ 回填缓存(旧值)
T3: 线程A 更新DB为新值
结果:缓存中是旧值,DB中是(可能)新值go-zero 的选择:接受极低概率的不一致
- DB 写操作(行锁)是慢操作
- 读写并发的窗口期极短
- 真正解决需要 2PC/Paxos,过于复杂
- 在绝大多数业务场景下,短暂的不一致可以接受
💡 如果业务对强一致性有要求:先更新 DB → 再删除缓存,并设置合理过期时间作为兜底。
缓存穿透防护
sqlc 默认对不存在的数据也缓存(NotFoundExpiry 默认 1 分钟),防止频繁查询不存在的数据穿透到 DB。
缓存雪崩防护
- 缓存过期时间设置为基础值 + 随机偏移
- go-zero 使用 Redis 连接池,内部有重试机制
自定义 Model 扩展
usermodel_gen.go 提供了基本的 CRUD,复杂的查询在 usermodel.go 中扩展:
// usermodel.go
package model
import (
"context"
"fmt"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// 自定义接口扩展
type UserModel interface {
userModel // 嵌入自动生成的接口
FindByName(ctx context.Context, name string) (*User, error)
FindByIds(ctx context.Context, ids []int64) ([]*User, error)
}
type defaultUserModel struct {
*defaultUserModel // 嵌入自动生成的实现
}
// 按名称模糊查询(不缓存)
func (m *defaultUserModel) FindByName(ctx context.Context, name string) (*User, error) {
var user User
query := fmt.Sprintf("SELECT %s FROM %s WHERE name LIKE ? LIMIT 1",
userRows, m.table)
err := m.CachedConn.QueryRowNoCacheCtx(ctx, &user, query, "%"+name+"%")
return &user, err
}
// 批量按 ID 查询(不缓存)
func (m *defaultUserModel) FindByIds(ctx context.Context, ids []int64) ([]*User, error) {
var users []*User
query := fmt.Sprintf("SELECT %s FROM %s WHERE id IN (%s)",
userRows, m.table, placeholders(len(ids)))
err := m.CachedConn.QueryRowsNoCacheCtx(ctx, &users, query, ids...)
return users, err
}事务管理
基础事务
err := l.svcCtx.DB.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 所有操作使用 session,不要用全局 DB
_, err := session.ExecCtx(ctx, "UPDATE account SET balance = balance - ? WHERE id = ?", amount, fromId)
if err != nil {
return err
}
_, err = session.ExecCtx(ctx, "UPDATE account SET balance = balance + ? WHERE id = ?", amount, toId)
return err
})事务 + 缓存一致性
go-zero 中事务和缓存是分离的。如果 Model 层方法需要事务支持,需要在 Repository 层封装:
// Repository 层封装事务操作
func (r *UserRepository) Transfer(ctx context.Context, fromId, toId int64, amount float64) error {
return r.db.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
// 事务内的操作不用缓存
_, err := session.ExecCtx(ctx, "UPDATE account SET balance = balance - ? WHERE id = ?", amount, fromId)
if err != nil {
return err
}
_, err = session.ExecCtx(ctx, "UPDATE account SET balance = balance + ? WHERE id = ?", amount, toId)
if err != nil {
return err
}
// 事务提交后手动清除相关缓存
r.cache.Del(fmt.Sprintf("cache:user:id:%d", fromId))
r.cache.Del(fmt.Sprintf("cache:user:id:%d", toId))
return nil
})
}go-zero + GORM 混合方案
如果你的团队更习惯 GORM:
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type ServiceContext struct {
Config config.Config
DB *gorm.DB
}
func NewServiceContext(c config.Config) *ServiceContext {
db, err := gorm.Open(mysql.Open(c.DataSource), &gorm.Config{})
if err != nil {
panic(err)
}
return &ServiceContext{Config: c, DB: db}
}取舍对比
| 维度 | go-zero 原生 (sqlx/sqlc) | GORM |
|---|---|---|
| 缓存管理 | ✅ 内置 Redis 缓存,自动一致性 | ❌ 需自行实现 |
| 代码生成 | ✅ goctl 一键 CRUD+缓存 | ✅ gorm/gen |
| 复杂关联查询 | 手写 SQL | 链式 + Preload |
| SQL 可控性 | ⚡ 精确控制 | 黑盒生成 |
| ORM 开销 | 几乎为零 | 有一定反射开销 |
| 社区生态 | 较小 | 极其庞大 |
💡 建议:对于标准 CRUD,用 goctl 生成的 sqlc Model;对于复杂关联查询,在 Repository 层手写 SQL 或用 GORM。
缓存最佳实践
| 实践 | 说明 |
|---|---|
| 只删不更新缓存 | 更新数据时删除缓存,读取时再回填 |
| 防缓存穿透 | sqlc 默认缓存空值 1 分钟 |
| 过期时间设置 | 热数据设长、冷数据设短,默认 7 天 |
| 批量查询不走缓存 | 用 QueryRowNoCache 直接查库 |
| 业务层缓存 | 列表等复杂查询,自行用 Redis Sorted Set/Hash |
| 缓存预热 | 启动时预加载热点数据到 Redis |
常见陷阱
| 陷阱 | 说明 |
|---|---|
| 🚨 认为 go-zero 自带事务控制 | 默认单条 SQL 无事务,多条 SQL 必须手动 TransactCtx |
| 🚨 事务内使用全局 DB 连接 | 事务内所有操作必须用 session,否则无法回滚 |
| 🚨 事务内做 RPC 调用 | 长事务占用锁导致并发阻塞,事务内只做 DB 操作 |
| 🚨 缓存永远不过期 | 异常情况下缓存可能永不过期,始终设置合理的过期时间 |
🚨 -table 参数大小写错误 |
MySQL 表名是小写,-table "User" 找不到表 |
🚨 手动修改 usermodel_gen.go |
这个文件 goctl 会覆盖,自定义逻辑写在 usermodel.go |
| 🚨 批量查询走缓存 | sqlc 不支持批量查询缓存,走了也不会命中 |
🚨 需要 SHOW CREATE TABLE 权限 |
goctl model 从数据库生成时需要此权限 |