Appearance
数据库与 Redis 集成
本篇讲解 go-zero 的数据访问层集成,包括 MySQL(内置 sqlx)、ORM(GORM)、Redis(内置 redisz)、以及 go-zero 内置的自动缓存机制。go-zero 在数据访问上做了大量工程化封装,特别是 model 层的代码生成和缓存集成,能显著降低开发成本。最后通过一个商品服务示例把 MySQL + Redis 缓存串起来。
一、数据库集成
1. 配置 MySQL 连接
go-zero 的数据库配置通过 sqlx 包管理。在 config.go 中定义:
go
// internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)
type Config struct {
rest.RestConf
MySQL struct {
DataSource string
}
CacheRedis cache.ClusterConf
}配置文件 etc/product-api.yaml:
yaml
Name: product-api
Host: 0.0.0.0
Port: 8888
MySQL:
DataSource: root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local
CacheRedis:
- Host: 127.0.0.1:6379
Type: nodeDataSource 字符串格式:user:password@tcp(host:port)/dbname?param=value。
常用参数:
charset=utf8mb4:字符集,支持 emojiparseTime=true:自动解析时间类型为time.Timeloc=Local:使用本地时区timeout=5s:连接超时readTimeout=5s:读超时writeTimeout=5s:写超时maxAllowedPacket=0:最大包大小
2. 使用 sqlx(go-zero 内置)
go-zero 的 sqlx 包是对标准库 database/sql 的封装,提供了更友好的 API。
创建连接
go
import "github.com/zeromicro/go-zero/core/stores/sqlx"
conn := sqlx.NewMysql(c.MySQL.DataSource)NewMysql 返回 sqlx.SqlConn 接口,它内部维护连接池。
基本查询
go
// 查询单行
var user User
err := conn.QueryRowCtx(ctx, &user, "SELECT id, name, email FROM users WHERE id=?", id)
// 查询多行
var users []User
err := conn.QueryRowsCtx(ctx, &users, "SELECT id, name, email FROM users WHERE age>?", 18)
// 插入
result, err := conn.ExecCtx(ctx, "INSERT INTO users(name,email,age) VALUES(?,?,?)", name, email, age)
// 更新
result, err := conn.ExecCtx(ctx, "UPDATE users SET name=? WHERE id=?", newName, id)
// 删除
result, err := conn.ExecCtx(ctx, "DELETE FROM users WHERE id=?", id)注意 Ctx 后缀的方法会自动传入 context.Context,用于超时控制和链路追踪。优先使用带 Ctx 的方法。
事务处理
go
err := conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
// 在事务中执行多条 SQL
if _, err := session.ExecCtx(ctx, "UPDATE account SET balance=balance-? WHERE user_id=?", amount, fromUserID); err != nil {
return err
}
if _, err := session.ExecCtx(ctx, "UPDATE account SET balance=balance+? WHERE user_id=?", amount, toUserID); err != nil {
return err
}
if _, err := session.ExecCtx(ctx, "INSERT INTO transfer_log(from_user,to_user,amount) VALUES(?,?,?)", fromUserID, toUserID, amount); err != nil {
return err
}
return nil
})
// 任意一步出错,整个事务回滚TransactCtx 接受一个闭包,闭包返回 error 时自动回滚,返回 nil 时自动提交。这种模式比手动 Begin/Commit/Rollback 更安全。
连接池配置
连接池参数通过 DataSource 的 param 控制,或者用 sqlx 的扩展配置:
go
import (
"github.com/zeromicro/go-zero/core/stores/sqlx"
"time"
)
// 通过 DSN 配置(推荐)
// DataSource: root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local&timeout=5s&readTimeout=5s&writeTimeout=5s
// go-zero 内部使用 database/sql,可以拿到底层 *sql.DB 配置
conn := sqlx.NewMysql(c.MySQL.DataSource)
db, err := conn.RawDB()
if err == nil {
db.SetMaxOpenConns(100) // 最大连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
db.SetConnMaxIdleTime(time.Minute * 30) // 空闲连接最大存活时间
}生产环境建议:
MaxOpenConns:根据 DB 的max_connections和服务实例数计算,留出余量MaxIdleConns:设为MaxOpenConns的 1/5 到 1/2ConnMaxLifetime:小于 DB 的wait_timeout,建议 30 分钟到 1 小时
3. 使用 goctl model 生成数据访问层
goctl 可以从 MySQL 表结构自动生成 model 代码,包含 CRUD 方法和缓存集成。
命令
bash
# 从数据库表生成(需要连接 MySQL)
goctl model mysql datasource -url "root:123456@tcp(127.0.0.1:3306)/demo" \
-table "users" \
-dir ./internal/model \
-cache true \
--style=goZero
# 从 SQL 文件生成
goctl model mysql ddl -src users.sql -dir ./internal/model -cache --style=goZero参数:
| 参数 | 说明 |
|---|---|
-url / -src | 数据库连接 / SQL 文件 |
-table | 表名 |
-dir | 输出目录 |
-cache | 是否生成缓存层 |
--style | 命名风格 |
生成的代码结构
internal/model/
├── usermodel.go # 接口定义和 NewUserModel
├── usermodel_gen.go # 自动生成的基础方法(不改)
├── usersModel.go # 自定义方法(手写扩展)
└── vars.go # 常量和变量生成的接口
go
// usermodel.go
type (
UserModel interface {
Insert(ctx context.Context, data *User) (sql.Result, error)
FindOne(ctx context.Context, id int64) (*User, error)
FindOneByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, data *User) error
Delete(ctx context.Context, id int64) error
}
defaultUserModel struct {
sqlx.SqlConn
cache.CachedConn // 内置缓存
table string
}
User struct {
Id int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Age int64 `db:"age"`
CreateTime time.Time `db:"create_time"`
UpdateTime time.Time `db:"update_time"`
}
)
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel {
return &defaultUserModel{
SqlConn: conn,
CachedConn: sqlc.NewConn(conn, c),
table: "users",
}
}扩展自定义方法
在 usersModel.go(自定义文件,不会被覆盖)中扩展:
go
package model
import (
"context"
"fmt"
)
func (m *defaultUserModel) FindList(ctx context.Context, keyword string, page, pageSize int) ([]*User, int64, error) {
offset := (page - 1) * pageSize
where := "WHERE 1=1"
args := []any{}
if keyword != "" {
where += " AND name LIKE ?"
args = append(args, "%"+keyword+"%")
}
query := fmt.Sprintf("SELECT id,name,email,age FROM %s %s LIMIT ?,?", m.table, where)
args = append(args, offset, pageSize)
var list []*User
err := m.CachedConn.QueryRowsCtx(ctx, &list, query, args...)
if err != nil {
return nil, 0, err
}
var total int64
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s %s", m.table, where)
countArgs := args[:len(args)-2] // 去掉 limit 参数
err = m.CachedConn.QueryRowCtx(ctx, &total, countQuery, countArgs...)
return list, total, err
}二、ORM 集成(GORM)
如果你更习惯 ORM 风格,go-zero 可以无缝集成 GORM。
1. 安装 GORM
bash
go get gorm.io/gorm
go get gorm.io/driver/mysql2. 配置与初始化
go
// internal/config/config.go
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
MySQL struct {
DataSource string
}
}go
// internal/svc/servicecontext.go
package svc
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"user-api/internal/config"
)
type ServiceContext struct {
Config config.Config
DB *gorm.DB
}
func NewServiceContext(c config.Config) *ServiceContext {
db, err := gorm.Open(mysql.Open(c.MySQL.DataSource), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
panic(err)
}
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
return &ServiceContext{
Config: c,
DB: db,
}
}3. 在 Logic 中使用
go
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
var user model.User
err = l.svcCtx.DB.WithContext(l.ctx).First(&user, req.Id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("user not found")
}
return nil, err
}
return &types.GetUserResponse{
Id: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}4. 事务
go
err := l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&Account{}).Where("user_id=?", fromUserID).
Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
return err
}
if err := tx.Model(&Account{}).Where("user_id=?", toUserID).
Update("balance", gorm.Expr("balance + ?", amount)).Error; err != nil {
return err
}
return tx.Create(&TransferLog{FromUserID: fromUserID, ToUserID: toUserID, Amount: amount}).Error
})5. sqlx vs GORM 选型
| 维度 | sqlx(go-zero 内置) | GORM |
|---|---|---|
| 性能 | 高(接近原生) | 中等(反射开销) |
| 学习成本 | 低(SQL 为主) | 中(需学 ORM API) |
| 缓存集成 | 内置 CachedConn | 需自行集成 |
| 代码生成 | goctl model 一键生成 | 需手写或用其他工具 |
| 灵活性 | 高(直接写 SQL) | 中(复杂查询需 Raw) |
| 适合场景 | 高性能、复杂 SQL | 快速开发、CRUD 为主 |
推荐:新项目优先用 sqlx + goctl model,与 go-zero 的缓存机制深度集成;已有 GORM 代码迁移可以继续用 GORM。
三、Redis 集成
1. 配置 Redis 连接
go
// internal/config/config.go
import "github.com/zeromicro/go-zero/core/stores/redis"
type Config struct {
rest.RestConf
Redis redis.RedisConf
}配置文件:
yaml
Redis:
Host: 127.0.0.1:6379
Type: node # node / cluster
Pass: "" # 密码(可选)
Tls: false # 是否启用 TLS
DB: 0 # db 编号集群模式:
yaml
Redis:
Type: cluster
Hosts:
- 10.0.0.1:6379
- 10.0.0.2:6379
- 10.0.0.3:6379
Pass: "your-password"2. 使用 redisz(go-zero 内置)
go-zero 的 redis 包封装了 go-redis,提供更友好的 API:
go
// internal/svc/servicecontext.go
import "github.com/zeromicro/go-zero/core/stores/redis"
type ServiceContext struct {
Config config.Config
Redis *redis.Redis
}
func NewServiceContext(c config.Config) *ServiceContext {
rds := redis.MustNewRedis(c.Redis)
return &ServiceContext{
Config: c,
Redis: rds,
}
}基本操作
go
rds := l.svcCtx.Redis
// String
rds.Set("key", "value")
rds.Setex("key", 3600, "value") // 带过期
val, err := rds.Get("key")
rds.Del("key")
// 计数器
rds.Incr("counter")
rds.Incrby("counter", 10)
// Hash
rds.Hset("user:1", "name", "alice")
name, _ := rds.Hget("user:1", "name")
rds.Hmset("user:1", map[string]string{"name":"alice","age":"25"})
// List
rds.Lpush("queue", "item1", "item2")
item, _ := rds.Rpop("queue")
// Set
rds.Sadd("tags", "go", "java")
members, _ := rds.Smembers("tags")
// 过期时间
rds.Expire("key", 60)
ttl, _ := rds.Ttl("key")
// 带上下文(推荐)
rds.SetCtx(ctx, "key", "value")
val, err := rds.GetCtx(ctx, "key")所有方法都有 Ctx 后缀版本,会传播 TraceID,建议优先使用。
3. 缓存模式:Cache-Aside
Cache-Aside 是最常用的缓存模式,流程:
读:先查缓存 → 命中返回;未命中查 DB → 写入缓存 → 返回
写:先写 DB → 删除缓存手动实现:
go
func (l *GetProductLogic) GetProduct(req *types.GetProductRequest) (*types.ProductResponse, error) {
cacheKey := fmt.Sprintf("product:%d", req.Id)
// 1. 查缓存
val, err := l.svcCtx.Redis.GetCtx(l.ctx, cacheKey)
if err == nil && val != "" {
var resp types.ProductResponse
if err := json.Unmarshal([]byte(val), &resp); err == nil {
return &resp, nil
}
}
// 2. 缓存未命中,查 DB
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, err
}
if product == nil {
return nil, fmt.Errorf("product not found")
}
resp := &types.ProductResponse{
Id: product.Id,
Name: product.Name,
Price: product.Price,
}
// 3. 写入缓存
if data, err := json.Marshal(resp); err == nil {
_ = l.svcCtx.Redis.SetexCtx(l.ctx, cacheKey, 3600, string(data))
}
return resp, nil
}但手动实现有诸多问题:缓存穿透、击穿、雪崩、一致性等。go-zero 提供了内置的自动缓存机制,下面介绍。
四、go-zero 内置缓存
go-zero 的 sqlc(CachedConn)和 cache 包提供了自动缓存能力,开发者几乎不需要手写缓存逻辑。
1. CachedConn 接口
CachedConn 是 go-zero 的核心缓存抽象,它把 DB 和 Redis 组合成一个「带缓存的连接」:
go
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// 创建带缓存的连接
conn := sqlc.NewConn(sqlx.NewMysql(c.MySQL.DataSource), c.CacheRedis)sqlc.NewConn 接收一个 sqlx.SqlConn 和缓存配置,返回 sqlc.CachedConn。
自动缓存读写
go
// 查询(自动走缓存)
var user User
err := conn.QueryRowCtx(ctx, &user, "SELECT * FROM users WHERE id=?", cacheKey, id)
// 第 4 个参数 cacheKey 是缓存键,goctl 生成的 model 会自动构造
// 插入(不涉及缓存)
result, err := conn.ExecCtx(ctx, "INSERT INTO users(name,email) VALUES(?,?)", name, email)
// 更新(自动删除缓存)
err := conn.ExecCtx(ctx, "UPDATE users SET name=? WHERE id=?", newName, id)
// 注意:这里需要配合 model 的 Update 方法,它会调用 DeleteCache 删除旧缓存2. goctl 生成的 model 缓存
用 goctl model mysql -cache true 生成的 model 自带缓存:
go
// usermodel_gen.go(goctl 生成)
func (m *defaultUserModel) Insert(ctx context.Context, data *User) (sql.Result, error) {
query := fmt.Sprintf("INSERT INTO %s (name,email,age) VALUES (?,?,?)", m.table)
ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
return conn.ExecCtx(ctx, query, data.Name, data.Email, data.Age)
}, fmt.Sprintf("user:%d", data.Id)) // 缓存 key
return ret, err
}
func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
var resp User
err := m.QueryRowCtx(ctx, &resp, userIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
query := fmt.Sprintf("SELECT * FROM %s WHERE id=? LIMIT 1", m.table)
return conn.QueryRowCtx(ctx, v, query, id)
})
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultUserModel) Update(ctx context.Context, data *User) error {
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
_, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
query := fmt.Sprintf("UPDATE %s SET name=?,email=?,age=? WHERE id=?", m.table)
return conn.ExecCtx(ctx, query, data.Name, data.Email, data.Age, data.Id)
}, userIdKey) // 更新后自动删除该 key
return err
}可以看到:
FindOne先查缓存,未命中再查 DB,查到后写入缓存Update/Delete在执行 SQL 后自动删除对应缓存 key- 缓存 key 由
cacheXxxPrefix + id构成
3. 缓存穿透、击穿、雪崩防护
go-zero 内置缓存机制已经处理了这些常见问题:
| 问题 | 防护机制 |
|---|---|
| 缓存穿透 | 查不到数据时缓存 nil(空值缓存),下次同样查询直接返回 |
| 缓存击穿 | 内置 singleflight,同一个 key 并发查询只会打到 DB 一次 |
| 缓存雪崩 | 缓存过期时间上加随机抖动,避免同时失效 |
sqlc 在 QueryRowCtx 时:
- 先查 Redis
- 未命中则用
singleflight合并并发请求 - singleflight 内查 DB
- DB 查不到则缓存
sqlc.ErrNotFound标记(空值缓存) - 查到则缓存真实数据
这些都不需要开发者手动处理。
4. 缓存一致性
go-zero 采用「先写 DB,再删缓存」策略(Cache-Aside 的标准做法):
go
func (m *defaultUserModel) Update(ctx context.Context, data *User) error {
// 1. 先更新 DB
// 2. 再删除缓存
}这种方式在绝大多数场景下足够。对一致性要求极高的场景(如金融),可以用:
- 延迟双删:先删缓存 → 写 DB → 延迟一段时间再删缓存
- 订阅 binlog:用 canal 订阅 MySQL binlog,异步更新缓存
五、完整示例:商品服务(MySQL + Redis 缓存)
下面实现一个完整的商品服务,演示 MySQL + go-zero 内置缓存。
1. 准备数据库
sql
CREATE DATABASE IF NOT EXISTS demo;
USE demo;
CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO products(name, description, price, stock) VALUES
('iPhone 15', '苹果手机', 5999.00, 100),
('MacBook Pro', '苹果笔记本', 12999.00, 50),
('AirPods Pro', '苹果耳机', 1999.00, 200);2. 生成 model
bash
goctl model mysql datasource \
-url "root:123456@tcp(127.0.0.1:3306)/demo" \
-table "products" \
-dir ./internal/model \
-cache true \
--style=goZero生成 internal/model/productsmodel.go、productsmodel_gen.go 等。
3. 项目配置
etc/product-api.yaml:
yaml
Name: product-api
Host: 0.0.0.0
Port: 8888
MySQL:
DataSource: root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=true&loc=Local
CacheRedis:
- Host: 127.0.0.1:6379
Type: nodego
// internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/rest"
)
type Config struct {
rest.RestConf
MySQL struct {
DataSource string
}
CacheRedis cache.ClusterConf
}4. ServiceContext
go
// internal/svc/servicecontext.go
package svc
import (
"github.com/zeromicro/go-zero/core/stores/sqlx"
"product-api/internal/config"
"product-api/internal/model"
)
type ServiceContext struct {
Config config.Config
ProductModel model.ProductModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.MySQL.DataSource)
return &ServiceContext{
Config: c,
ProductModel: model.NewProductsModel(conn, c.CacheRedis),
}
}5. .api 文件
api
syntax = "v1"
info (
title: "商品服务"
version: "v1"
)
type (
ProductVO {
Id int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
GetProductRequest {
Id int64 `path:"id"`
}
GetProductResponse {
Product ProductVO `json:"product"`
}
ListProductRequest {
Page int `form:"page,default=1"`
PageSize int `form:"pageSize,default=10"`
Keyword string `form:"keyword,optional"`
}
ListProductResponse {
Total int `json:"total"`
List []ProductVO `json:"list"`
}
CreateProductRequest {
Name string `json:"name"`
Description string `json:"description,optional"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
CreateProductResponse {
Id int64 `json:"id"`
}
UpdateProductRequest {
Id int64 `path:"id"`
Name string `json:"name,optional"`
Description string `json:"description,optional"`
Price float64 `json:"price,optional"`
Stock int `json:"stock,optional"`
}
DeleteProductRequest {
Id int64 `path:"id"`
}
)
@server (
group: product
prefix: /api/v1
)
service product-api {
@handler GetProductHandler
get /product/:id (GetProductRequest) returns (GetProductResponse)
@handler ListProductHandler
get /product (ListProductRequest) returns (ListProductResponse)
@handler CreateProductHandler
post /product (CreateProductRequest) returns (CreateProductResponse)
@handler UpdateProductHandler
put /product/:id (UpdateProductRequest) returns (BaseResponse)
@handler DeleteProductHandler
delete /product/:id (DeleteProductRequest) returns (BaseResponse)
}
type (
BaseResponse {
Code int `json:"code"`
Msg string `json:"msg"`
}
)6. Logic 实现
go
// internal/logic/product/getproductlogic.go
package product
import (
"context"
"fmt"
"product-api/internal/svc"
"product-api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetProductLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductLogic {
return &GetProductLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetProductLogic) GetProduct(req *types.GetProductRequest) (resp *types.GetProductResponse, err error) {
// 这里会自动走缓存:先查 Redis,未命中查 DB,再写回缓存
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, err
}
if product == nil {
return nil, fmt.Errorf("product not found")
}
return &types.GetProductResponse{
Product: types.ProductVO{
Id: product.Id,
Name: product.Name,
Description: product.Description,
Price: product.Price,
Stock: int(product.Stock),
},
}, nil
}go
// internal/logic/product/createproductlogic.go
func (l *CreateProductLogic) CreateProduct(req *types.CreateProductRequest) (resp *types.CreateProductResponse, err error) {
result, err := l.svcCtx.ProductModel.Insert(l.ctx, &model.Products{
Name: req.Name,
Description: req.Description,
Price: req.Price,
Stock: int64(req.Stock),
})
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return &types.CreateProductResponse{Id: id}, nil
}go
// internal/logic/product/updateproductlogic.go
func (l *UpdateProductLogic) UpdateProduct(req *types.UpdateProductRequest) (resp *types.BaseResponse, err error) {
// 先查出原数据(走缓存)
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id)
if err != nil {
return nil, err
}
if product == nil {
return nil, fmt.Errorf("product not found")
}
// 更新字段
if req.Name != "" {
product.Name = req.Name
}
if req.Description != "" {
product.Description = req.Description
}
if req.Price != 0 {
product.Price = req.Price
}
if req.Stock != 0 {
product.Stock = int64(req.Stock)
}
// 更新(自动删除缓存)
err = l.svcCtx.ProductModel.Update(l.ctx, product)
if err != nil {
return nil, err
}
return &types.BaseResponse{Code: 0, Msg: "ok"}, nil
}7. 验证缓存效果
bash
# 第一次查询(查 DB,写入缓存)
curl http://localhost:8888/api/v1/product/1
# Redis 中会出现 key: cache:product:id:1
# 第二次查询(直接走缓存)
curl http://localhost:8888/api/v1/product/1
# 速度明显更快
# 更新(删除缓存)
curl -X PUT http://localhost:8888/api/v1/product/1 \
-H "Content-Type: application/json" \
-d '{"price":5999.00}'
# 再次查询(缓存已删,重新查 DB 写缓存)
curl http://localhost:8888/api/v1/product/1可以观察日志,看到只有第一次和更新后查询会真正打到 DB。
六、Redis 高级用法
1. 分布式锁
go
import "github.com/zeromicro/go-zero/core/stores/redis"
// 获取锁
lock := redis.NewRedisLock(l.svcCtx.Redis, "lock:order:123")
lock.SetExpire(10) // 锁过期时间 10 秒
acquired, err := lock.Acquire()
if err != nil {
return err
}
if !acquired {
return fmt.Errorf("failed to acquire lock")
}
defer lock.Release()
// 执行业务
doBusiness()2. 布隆过滤器
go
import "github.com/zeromicro/go-zero/core/bloom"
filter := bloom.New(l.svcCtx.Redis, "bloom:user", 64*1024*1024)
filter.Add([]byte("user@example.com"))
exists, _ := filter.Exists([]byte("user@example.com"))布隆过滤器用于防止缓存穿透(在缓存层前面加一层判断)。
3. 限流
go
import "github.com/zeromicro/go-zero/core/limit"
// 周期限流(每秒 100 次)
limiter := limit.NewPeriodLimit(1, 100, l.svcCtx.Redis, "rate_limit")
code, _ := limiter.Take("user:123")
if code == limit.OverQuota {
return fmt.Errorf("rate limited")
}
// 令牌桶限流
tokenLimiter := limit.NewTokenLimit(100, 200, l.svcCtx.Redis)
if !tokenLimiter.Allow() {
return fmt.Errorf("rate limited")
}七、缓存设计最佳实践
1. 缓存 key 设计
- 使用业务前缀:
user:1001、product:2001 - 避免冲突:不同实体用不同前缀
- 控制长度:key 越短,Redis 内存占用越小
2. 过期时间
- 热点数据:长一些(1-6 小时)
- 不活跃数据:短一些(5-30 分钟)
- 永不变数据:可以不过期(如配置类)
- 加随机抖动:避免雪崩
3. 序列化
go-zero 内置缓存用 json 序列化。如果对性能敏感,可以用 protobuf 或 msgpack:
go
// 自定义序列化(手动管理缓存时)
data, _ := msgpack.Marshal(product)
l.svcCtx.Redis.SetexCtx(ctx, key, 3600, string(data))4. 缓存预热
服务启动或低峰期,主动把热点数据加载到缓存:
go
func WarmupCache(ctx context.Context, model ProductModel, rds *redis.Redis) error {
products, _ := model.FindHotProducts(ctx, 100)
for _, p := range products {
key := fmt.Sprintf("product:%d", p.Id)
data, _ := json.Marshal(p)
rds.SetexCtx(ctx, key, 3600, string(data))
}
return nil
}八、小结
本篇讲解了 go-zero 的数据库与 Redis 集成。要点回顾:
sqlx是 go-zero 内置的 SQL 封装,支持Ctx上下文、事务、连接池goctl model mysql一键生成 model 代码(含 CRUD + 缓存)- GORM 可无缝集成,适合喜欢 ORM 风格的团队
redis包封装了 go-redis,API 友好,支持单机/集群sqlc.CachedConn提供自动缓存,结合 singleflight 防击穿、空值缓存防穿透- goctl model 自动采用「先写 DB 再删缓存」策略
- 完整商品服务示例演示了 MySQL + Redis 的端到端实践
- Redis 还可用于分布式锁、布隆过滤器、限流等场景
下一篇我们将深入模型缓存设计,详解 CachedConn 原理、缓存穿透/击穿/雪崩的防护机制,并通过高并发查询场景对比性能。