Skip to content

数据库与 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: node

DataSource 字符串格式:user:password@tcp(host:port)/dbname?param=value

常用参数:

  • charset=utf8mb4:字符集,支持 emoji
  • parseTime=true:自动解析时间类型为 time.Time
  • loc=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/2
  • ConnMaxLifetime:小于 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/mysql

2. 配置与初始化

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 一次
缓存雪崩缓存过期时间上加随机抖动,避免同时失效

sqlcQueryRowCtx 时:

  1. 先查 Redis
  2. 未命中则用 singleflight 合并并发请求
  3. singleflight 内查 DB
  4. DB 查不到则缓存 sqlc.ErrNotFound 标记(空值缓存)
  5. 查到则缓存真实数据

这些都不需要开发者手动处理。

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.goproductsmodel_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: node
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
}

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:1001product:2001
  • 避免冲突:不同实体用不同前缀
  • 控制长度:key 越短,Redis 内存占用越小

2. 过期时间

  • 热点数据:长一些(1-6 小时)
  • 不活跃数据:短一些(5-30 分钟)
  • 永不变数据:可以不过期(如配置类)
  • 加随机抖动:避免雪崩

3. 序列化

go-zero 内置缓存用 json 序列化。如果对性能敏感,可以用 protobufmsgpack

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 原理、缓存穿透/击穿/雪崩的防护机制,并通过高并发查询场景对比性能。