Files
juwan-backend/docs/gozero-redis-configuration.md

32 KiB
Raw Permalink Blame History

Go-Zero 框架 Redis 配置完全指南

框架版本: go-zero v1.5+
Redis 版本: 7.0.12
部署环境: Kubernetes (juwan namespace)
文档日期: 2026年2月22日


📋 目录

  1. 配置概览
  2. 单节点模式
  3. Sentinel 哨兵模式
  4. 集群模式
  5. 配置项详解
  6. 代码实现
  7. 常用操作示例
  8. 高级特性
  9. 性能优化
  10. 故障排查
  11. 最佳实践

🎯 配置概览

Go-Zero Redis 支持的模式

模式 Type 值 用途 高可用 推荐度
单节点 node 开发/测试
Sentinel sentinel 生产环境
集群 cluster 大规模分片

配置文件位置

app/users/rpc/
├── etc/
│   └── pb.yaml          ← Redis 配置写在这里
├── internal/
│   ├── config/
│   │   └── config.go    ← 定义配置结构
│   └── svc/
│       └── serviceContext.go  ← 初始化 Redis 客户端
└── pb.go

🔵 单节点模式

适用场景

  • 开发环境
  • 测试环境
  • POC 演示
  • 生产环境(无高可用)

配置文件

app/users/rpc/etc/pb.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9001

# Redis 单节点配置
Redis:
  Host: user-redis-master.juwan.svc.cluster.local:6379
  Type: node
  Pass: ${REDIS_PASSWORD}  # 从环境变量读取
  # Db: 0                  # 可选,默认 0
  # MaxIdle: 8             # 可选,连接池最大闲置连接数
  # MaxActive: 0           # 可选,连接池最大活跃连接数,0 表示无限制

Etcd:
  Hosts:
    - etcd-service.juwan.svc.cluster.local:2379
  Key: user.rpc

Config 结构

app/users/rpc/internal/config/config.go

package config

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
	zrpc.RpcServerConf
	Redis redis.RedisConf  // Redis 配置
}

ServiceContext 初始化

app/users/rpc/internal/svc/serviceContext.go

package svc

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"juwan-backend/app/users/rpc/internal/config"
)

type ServiceContext struct {
	Config config.Config
	Redis  *redis.Redis
}

func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config: c,
		Redis:  redis.MustNewRedis(c.Redis),  // 初始化 Redis
	}
}

使用示例

app/users/rpc/internal/logic/getUsersByIdLogic.go

package logic

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"juwan-backend/app/users/rpc/internal/svc"
	"juwan-backend/app/users/rpc/pb"

	"github.com/zeromicro/go-zero/core/logx"
)

type GetUsersByIdLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

func NewGetUsersByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUsersByIdLogic {
	return &GetUsersByIdLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) {
	// 缓存 key
	cacheKey := fmt.Sprintf("user:%d", in.Id)
	
	// 1. 尝试从缓存获取
	cached, err := l.svcCtx.Redis.Get(cacheKey)
	if err == nil && cached != "" {
		// 缓存命中
		var user pb.User
		if err := json.Unmarshal([]byte(cached), &user); err == nil {
			l.Logger.Infof("Cache hit for user:%d", in.Id)
			return &pb.GetUsersByIdResp{User: &user}, nil
		}
	}
	
	// 2. 缓存未命中,从数据库查询
	l.Logger.Infof("Cache miss for user:%d, querying DB", in.Id)
	user := l.fetchUserFromDB(in.Id)
	if user == nil {
		return nil, fmt.Errorf("user not found")
	}
	
	// 3. 写入缓存(1小时过期)
	userJSON, _ := json.Marshal(user)
	err = l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600)
	if err != nil {
		l.Logger.Errorf("Failed to set cache: %v", err)
	}
	
	return &pb.GetUsersByIdResp{User: user}, nil
}

func (l *GetUsersByIdLogic) fetchUserFromDB(id int64) *pb.User {
	// 实际从数据库查询
	// ...
	return &pb.User{Id: id, Name: "John Doe"}
}

🟡 Sentinel 哨兵模式

适用场景

  • 生产环境强烈推荐
  • 自动故障转移
  • 高可用架构
  • 主从自动切换

配置文件

app/users/rpc/etc/pb.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9001

# Redis Sentinel 配置(推荐生产环境使用)
Redis:
  - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
  Type: sentinel
  Pass: ${REDIS_PASSWORD}

# 或者使用完整配置
# Redis:
#   Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
#   Type: sentinel
#   Pass: ${REDIS_PASSWORD}
#   # Sentinel 特有配置
#   MasterName: mymaster  # Sentinel 主节点名称,默认 mymaster

Etcd:
  Hosts:
    - etcd-service.juwan.svc.cluster.local:2379
  Key: user.rpc

Config 结构(同单节点)

app/users/rpc/internal/config/config.go

package config

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
	zrpc.RpcServerConf
	Redis redis.RedisConf  // 支持所有模式
}

ServiceContext 初始化(同单节点)

app/users/rpc/internal/svc/serviceContext.go

package svc

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"juwan-backend/app/users/rpc/internal/config"
)

type ServiceContext struct {
	Config config.Config
	Redis  *redis.Redis
}

func NewServiceContext(c config.Config) *ServiceContext {
	// go-zero 会根据 Type 自动选择连接模式
	return &ServiceContext{
		Config: c,
		Redis:  redis.MustNewRedis(c.Redis),
	}
}

Sentinel 配置详解

完整配置选项:

Redis:
  Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
  Type: sentinel
  Pass: ${REDIS_PASSWORD}
  
  # Sentinel 特有配置
  MasterName: mymaster          # Sentinel 监控的主节点名称
  
  # 连接池配置(可选)
  MaxIdle: 8                    # 最大闲置连接数
  MaxActive: 0                  # 最大活跃连接数,0 表示无限制
  IdleTimeout: 300              # 闲置连接超时时间(秒)
  
  # 超时配置(可选)
  ConnectTimeout: 5000          # 连接超时(毫秒)
  ReadTimeout: 3000             # 读超时(毫秒)
  WriteTimeout: 3000            # 写超时(毫秒)

优势说明

// Sentinel 模式的自动故障处理流程:

// 1. 应用连接到 Sentinel
app  Sentinel Service (26379)

// 2. Sentinel 返回当前主节点地址
Sentinel  app: "主节点在 10.244.1.10:6379"

// 3. 应用连接到主节点进行读写
app  Redis Master (10.244.1.10:6379)

// 4. 主节点故障,Sentinel 检测到
Redis Master ( 宕机)
Sentinel  检测  投票  提升新主节点

// 5. 应用下次请求时自动连接到新主节点
app  Sentinel  "新主节点在 10.244.2.20:6379"
app  New Redis Master (10.244.2.20:6379)

// 整个过程应用无需重启,自动完成切换!

🔴 集群模式

适用场景

  • 大规模数据(需要分片)
  • 超高并发
  • 数据量超过单机内存
  • ⚠️ 配置和运维复杂度高

配置文件

app/users/rpc/etc/pb.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9001

# Redis Cluster 配置
Redis:
  - Host: redis-cluster-0.redis-cluster.juwan.svc.cluster.local:6379
  - Host: redis-cluster-1.redis-cluster.juwan.svc.cluster.local:6379
  - Host: redis-cluster-2.redis-cluster.juwan.svc.cluster.local:6379
  Type: cluster
  Pass: ${REDIS_PASSWORD}

Etcd:
  Hosts:
    - etcd-service.juwan.svc.cluster.local:2379
  Key: user.rpc

Config 结构

app/users/rpc/internal/config/config.go

package config

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
	zrpc.RpcServerConf
	Redis redis.RedisConf
}

集群模式特点

数据分片:

应用请求
    ↓
根据 key 计算 hash slot (0-16383)
    ↓
路由到对应的分片节点
    ↓
┌─────────┬─────────┬─────────┐
│ Shard 1 │ Shard 2 │ Shard 3 │
│ 0-5460  │5461-10922│10923-16383│
└─────────┴─────────┴─────────┘

注意事项:

  • 不支持多 key 操作(如 MGET, MSET)跨分片
  • 不支持事务(MULTI/EXEC)跨分片
  • 单 key 操作完全正常
  • 支持 hash tag 控制 key 分布

📚 配置项详解

redis.RedisConf 完整配置

结构定义:

type RedisConf struct {
    Host        string        // Redis 地址
    Type        string        // 类型: node, sentinel, cluster
    Pass        string        // 密码
    Db          int           // 数据库编号 (0-15)cluster 模式不支持
    
    // Sentinel 模式专用
    MasterName  string        // Sentinel 主节点名称
    
    // 连接池配置
    MaxIdle     int           // 最大闲置连接数
    MaxActive   int           // 最大活跃连接数,0 表示无限制
    IdleTimeout time.Duration // 闲置连接超时
    
    // 超时配置
    ConnectTimeout time.Duration // 连接超时
    ReadTimeout    time.Duration // 读超时
    WriteTimeout   time.Duration // 写超时
    
    // TLS 配置(可选)
    Tls    bool   // 是否启用 TLS
}

各配置项说明

1. Host(必填)

单节点模式:

Redis:
  Host: user-redis-master.juwan.svc.cluster.local:6379

Sentinel 模式:

Redis:
  Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
  # 注意:这里填 Sentinel 地址(端口 26379),不是 Redis 地址

集群模式:

Redis:
  # 可以填任意一个节点,客户端会自动发现其他节点
  - Host: redis-cluster-0:6379
  - Host: redis-cluster-1:6379
  - Host: redis-cluster-2:6379

2. Type(必填)

说明
node 单节点模式
sentinel Sentinel 哨兵模式
cluster 集群模式

3. Pass(强烈推荐)

从环境变量读取(推荐):

Redis:
  Pass: ${REDIS_PASSWORD}

硬编码(不推荐):

Redis:
  Pass: "your-password"  # ❌ 不安全

4. Db(可选,默认 0

适用模式:nodesentinel 模式

Redis:
  Db: 0  # 数据库编号 0-15

注意:

  • Cluster 模式不支持多数据库
  • 单节点和 Sentinel 支持 0-15

5. MaxIdle(可选,默认 8

Redis:
  MaxIdle: 8  # 连接池中最大闲置连接数

建议值:

  • 低并发:8
  • 中并发:16
  • 高并发:32CPU 核心数 * 2

6. MaxActive(可选,默认 0

Redis:
  MaxActive: 0  # 0 表示无限制
  # MaxActive: 100  # 或设置一个上限

建议值:

  • 开发环境:0(无限制)
  • 生产环境:100-500(根据实际负载)

7. IdleTimeout(可选,默认 300 秒)

Redis:
  IdleTimeout: 300  # 秒

说明: 闲置连接超过此时间会被关闭

8. 超时配置(可选)

Redis:
  ConnectTimeout: 5000  # 连接超时 5 秒
  ReadTimeout: 3000     # 读超时 3 秒
  WriteTimeout: 3000    # 写超时 3 秒

💻 代码实现

完整项目结构

app/users/rpc/
├── etc/
│   ├── pb.yaml           # 开发环境配置
│   └── pb-prod.yaml      # 生产环境配置
├── internal/
│   ├── config/
│   │   └── config.go     # 配置结构定义
│   ├── svc/
│   │   └── serviceContext.go  # 服务上下文(初始化 Redis)
│   ├── logic/
│   │   ├── addUsersLogic.go
│   │   ├── getUsersByIdLogic.go
│   │   └── ...
│   └── server/
│       └── usercenterServer.go
├── pb/
│   ├── users.pb.go
│   └── users_grpc.pb.go
└── usercenter.go         # 主程序入口

1. 配置文件示例

开发环境 etc/pb.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9001

# 开发环境使用单节点
Redis:
  Host: localhost:6379
  Type: node
  Pass: dev_password
  Db: 0

Etcd:
  Hosts:
    - localhost:2379
  Key: user.rpc

Log:
  Level: info
  Mode: console

生产环境 etc/pb-prod.yaml

Name: user.rpc
ListenOn: 0.0.0.0:9001

# 生产环境使用 Sentinel
Redis:
  Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
  Type: sentinel
  Pass: ${REDIS_PASSWORD}
  MasterName: mymaster
  MaxIdle: 16
  MaxActive: 100
  IdleTimeout: 300
  ConnectTimeout: 5000
  ReadTimeout: 3000
  WriteTimeout: 3000

Etcd:
  Hosts:
    - etcd-0.etcd.juwan.svc.cluster.local:2379
    - etcd-1.etcd.juwan.svc.cluster.local:2379
    - etcd-2.etcd.juwan.svc.cluster.local:2379
  Key: user.rpc

Log:
  Level: error
  Mode: file
  Path: /var/log/user-rpc
  KeepDays: 7

2. Config 定义

internal/config/config.go

package config

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
	zrpc.RpcServerConf
	
	// Redis 配置
	Redis redis.RedisConf
	
	// 其他配置...
	// DB postgres.Config
	// Kafka kafka.Config
}

3. ServiceContext 初始化

internal/svc/serviceContext.go

package svc

import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"juwan-backend/app/users/rpc/internal/config"
)

type ServiceContext struct {
	Config config.Config
	Redis  *redis.Redis
	// 其他依赖...
	// DB *gorm.DB
}

func NewServiceContext(c config.Config) *ServiceContext {
	// 初始化 Redis(支持所有模式:node, sentinel, cluster
	rdb := redis.MustNewRedis(c.Redis)
	
	return &ServiceContext{
		Config: c,
		Redis:  rdb,
	}
}

4. 主程序入口

usercenter.go

package main

import (
	"flag"
	"fmt"

	"juwan-backend/app/users/rpc/internal/config"
	"juwan-backend/app/users/rpc/internal/server"
	"juwan-backend/app/users/rpc/internal/svc"
	"juwan-backend/app/users/rpc/pb"

	"github.com/zeromicro/go-zero/core/conf"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/zrpc"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

var configFile = flag.String("f", "etc/pb.yaml", "the config file")

func main() {
	flag.Parse()

	// 加载配置
	var c config.Config
	conf.MustLoad(*configFile, &c)
	
	// 初始化服务上下文
	ctx := svc.NewServiceContext(c)
	
	// 创建 gRPC 服务
	s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
		pb.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx))

		if c.Mode == service.DevMode || c.Mode == service.TestMode {
			reflection.Register(grpcServer)
		}
	})
	defer s.Stop()

	fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
	s.Start()
}

🔧 常用操作示例

1. 基本读写操作

package logic

import (
	"context"
	"fmt"
	"time"
)

type UserLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	logx.Logger
}

// Set 操作
func (l *UserLogic) SetUser(userId int64, data string) error {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Set(key, data)
}

// Setex 操作(带过期时间)
func (l *UserLogic) SetUserWithExpiry(userId int64, data string) error {
	key := fmt.Sprintf("user:%d", userId)
	// 缓存 1 小时
	return l.svcCtx.Redis.Setex(key, data, 3600)
}

// Get 操作
func (l *UserLogic) GetUser(userId int64) (string, error) {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Get(key)
}

// Del 操作
func (l *UserLogic) DeleteUser(userId int64) error {
	key := fmt.Sprintf("user:%d", userId)
	_, err := l.svcCtx.Redis.Del(key)
	return err
}

// Exists 检查
func (l *UserLogic) UserExists(userId int64) (bool, error) {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Exists(key)
}

2. Hash 操作

// HSet 操作
func (l *UserLogic) SetUserField(userId int64, field, value string) error {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Hset(key, field, value)
}

// HGet 操作
func (l *UserLogic) GetUserField(userId int64, field string) (string, error) {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Hget(key, field)
}

// HGetAll 操作
func (l *UserLogic) GetAllUserFields(userId int64) (map[string]string, error) {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Hgetall(key)
}

// HMSet 批量设置
func (l *UserLogic) SetUserFields(userId int64, fields map[string]string) error {
	key := fmt.Sprintf("user:%d", userId)
	return l.svcCtx.Redis.Hmset(key, fields)
}

3. List 操作

// LPush 操作
func (l *UserLogic) AddMessage(userId int64, message string) error {
	key := fmt.Sprintf("messages:%d", userId)
	_, err := l.svcCtx.Redis.Lpush(key, message)
	return err
}

// LRange 操作
func (l *UserLogic) GetMessages(userId int64, start, stop int) ([]string, error) {
	key := fmt.Sprintf("messages:%d", userId)
	return l.svcCtx.Redis.Lrange(key, start, stop)
}

// LLen 操作
func (l *UserLogic) GetMessageCount(userId int64) (int, error) {
	key := fmt.Sprintf("messages:%d", userId)
	return l.svcCtx.Redis.Llen(key)
}

4. Set 操作

// SAdd 添加成员
func (l *UserLogic) AddUserTag(userId int64, tag string) error {
	key := fmt.Sprintf("user:tags:%d", userId)
	_, err := l.svcCtx.Redis.Sadd(key, tag)
	return err
}

// SMembers 获取所有成员
func (l *UserLogic) GetUserTags(userId int64) ([]string, error) {
	key := fmt.Sprintf("user:tags:%d", userId)
	return l.svcCtx.Redis.Smembers(key)
}

// SIsMember 检查成员
func (l *UserLogic) HasUserTag(userId int64, tag string) (bool, error) {
	key := fmt.Sprintf("user:tags:%d", userId)
	return l.svcCtx.Redis.Sismember(key, tag)
}

5. Sorted Set 操作

// ZAdd 添加成员
func (l *UserLogic) AddToLeaderboard(userId int64, score int64) error {
	key := "leaderboard"
	_, err := l.svcCtx.Redis.Zadd(key, score, fmt.Sprintf("%d", userId))
	return err
}

// ZRevRange 获取排行榜(从高到低)
func (l *UserLogic) GetTopUsers(count int) ([]string, error) {
	key := "leaderboard"
	return l.svcCtx.Redis.Zrevrange(key, 0, int64(count-1))
}

// ZRank 获取排名
func (l *UserLogic) GetUserRank(userId int64) (int64, error) {
	key := "leaderboard"
	return l.svcCtx.Redis.Zrank(key, fmt.Sprintf("%d", userId))
}

6. 缓存模式实现

Cache-Aside Pattern(推荐):

func (l *UserLogic) GetUserById(userId int64) (*User, error) {
	cacheKey := fmt.Sprintf("user:%d", userId)
	
	// 1. 查缓存
	cached, err := l.svcCtx.Redis.Get(cacheKey)
	if err == nil && cached != "" {
		var user User
		if err := json.Unmarshal([]byte(cached), &user); err == nil {
			return &user, nil
		}
	}
	
	// 2. 查数据库
	user, err := l.getUserFromDB(userId)
	if err != nil {
		return nil, err
	}
	
	// 3. 写缓存
	userJSON, _ := json.Marshal(user)
	l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600)
	
	return user, nil
}

func (l *UserLogic) UpdateUser(user *User) error {
	// 1. 更新数据库
	if err := l.updateUserInDB(user); err != nil {
		return err
	}
	
	// 2. 删除缓存(下次读取时会重新加载)
	cacheKey := fmt.Sprintf("user:%d", user.Id)
	l.svcCtx.Redis.Del(cacheKey)
	
	return nil
}

7. 分布式锁

// 获取分布式锁
func (l *UserLogic) AcquireLock(key string, expiry int) (bool, error) {
	lockKey := fmt.Sprintf("lock:%s", key)
	return l.svcCtx.Redis.Setnx(lockKey, "1")
}

// 释放锁
func (l *UserLogic) ReleaseLock(key string) error {
	lockKey := fmt.Sprintf("lock:%s", key)
	_, err := l.svcCtx.Redis.Del(lockKey)
	return err
}

// 使用示例
func (l *UserLogic) ProcessWithLock(userId int64) error {
	lockKey := fmt.Sprintf("user:%d", userId)
	
	// 获取锁
	acquired, err := l.AcquireLock(lockKey, 10)
	if err != nil {
		return err
	}
	if !acquired {
		return fmt.Errorf("failed to acquire lock")
	}
	defer l.ReleaseLock(lockKey)
	
	// 执行业务逻辑
	// ...
	
	return nil
}

8. Pipeline 批量操作

// 使用 go-redis 原生客户端进行 Pipeline
func (l *UserLogic) BatchSetUsers(users []*User) error {
	// go-zero 的 Redis 包装了 go-redis,可以获取原生客户端
	rdb := l.svcCtx.Redis
	
	pipe := rdb.Pipelined(func(pip redis.Pipeliner) error {
		for _, user := range users {
			key := fmt.Sprintf("user:%d", user.Id)
			userJSON, _ := json.Marshal(user)
			pip.Set(context.Background(), key, userJSON, time.Hour)
		}
		return nil
	})
	
	return pipe
}

🚀 高级特性

1. 缓存穿透防护(布隆过滤器)

import "github.com/zeromicro/go-zero/core/bloom"

type UserLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
	filter *bloom.Filter
}

func (l *UserLogic) GetUserWithBloom(userId int64) (*User, error) {
	// 1. 布隆过滤器检查
	if !l.filter.Exists([]byte(fmt.Sprintf("%d", userId))) {
		return nil, fmt.Errorf("user not found")
	}
	
	// 2. 查缓存
	cacheKey := fmt.Sprintf("user:%d", userId)
	cached, err := l.svcCtx.Redis.Get(cacheKey)
	if err == nil && cached != "" {
		var user User
		json.Unmarshal([]byte(cached), &user)
		return &user, nil
	}
	
	// 3. 查数据库
	user, err := l.getUserFromDB(userId)
	if err != nil {
		return nil, err
	}
	
	// 4. 写缓存
	userJSON, _ := json.Marshal(user)
	l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600)
	
	return user, nil
}

2. 缓存击穿防护(Singleflight

import "golang.org/x/sync/singleflight"

type UserLogic struct {
	ctx context.Context
	svcCtx *svc.ServiceContext
	sg singleflight.Group
}

func (l *UserLogic) GetUserWithSingleflight(userId int64) (*User, error) {
	cacheKey := fmt.Sprintf("user:%d", userId)
	
	// 使用 Singleflight 确保同一时刻只有一个请求查询
	v, err, _ := l.sg.Do(cacheKey, func() (interface{}, error) {
		// 1. 查缓存
		cached, err := l.svcCtx.Redis.Get(cacheKey)
		if err == nil && cached != "" {
			var user User
			json.Unmarshal([]byte(cached), &user)
			return &user, nil
		}
		
		// 2. 查数据库
		user, err := l.getUserFromDB(userId)
		if err != nil {
			return nil, err
		}
		
		// 3. 写缓存
		userJSON, _ := json.Marshal(user)
		l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600)
		
		return user, nil
	})
	
	if err != nil {
		return nil, err
	}
	
	return v.(*User), nil
}

3. 缓存雪崩防护(随机过期时间)

import (
	"math/rand"
	"time"
)

func (l *UserLogic) SetCacheWithRandomExpiry(key string, value string, baseExpiry int) error {
	// 在基础过期时间上增加随机值(±20%)
	randomOffset := rand.Intn(baseExpiry / 5)
	expiry := baseExpiry + randomOffset - (baseExpiry / 10)
	
	return l.svcCtx.Redis.Setex(key, value, expiry)
}

// 使用示例
func (l *UserLogic) CacheUser(user *User) error {
	key := fmt.Sprintf("user:%d", user.Id)
	userJSON, _ := json.Marshal(user)
	
	// 基础过期时间 1 小时,实际会在 48-72 分钟之间随机
	return l.SetCacheWithRandomExpiry(key, string(userJSON), 3600)
}

性能优化

1. 连接池配置优化

根据并发量调整:

# 低并发(< 100 QPS
Redis:
  MaxIdle: 8
  MaxActive: 100

# 中并发(100-1000 QPS
Redis:
  MaxIdle: 16
  MaxActive: 500

# 高并发(> 1000 QPS
Redis:
  MaxIdle: 32
  MaxActive: 1000

2. 超时配置优化

Redis:
  # 连接超时:通常设置较大值
  ConnectTimeout: 5000  # 5 秒
  
  # 读写超时:设置较小值,快速失败
  ReadTimeout: 1000     # 1 秒
  WriteTimeout: 1000    # 1 秒

3. Pipeline 批量操作

避免循环调用:

// ❌ 不好:循环调用
for _, user := range users {
	key := fmt.Sprintf("user:%d", user.Id)
	l.svcCtx.Redis.Set(key, user.Name)
}

// ✅ 推荐:使用 Pipeline
pipe := l.svcCtx.Redis.Pipelined(func(pip redis.Pipeliner) error {
	for _, user := range users {
		key := fmt.Sprintf("user:%d", user.Id)
		pip.Set(context.Background(), key, user.Name, 0)
	}
	return nil
})

4. 合理的缓存过期时间

const (
	CacheExpiryShort  = 300    // 5 分钟 - 热点数据
	CacheExpiryMedium = 3600   // 1 小时 - 常规数据
	CacheExpiryLong   = 86400  // 1 天 - 冷数据
)

func (l *UserLogic) SetUserCache(user *User, expiry int) error {
	key := fmt.Sprintf("user:%d", user.Id)
	userJSON, _ := json.Marshal(user)
	return l.svcCtx.Redis.Setex(key, string(userJSON), expiry)
}

5. Key 命名规范

// 推荐的 Key 命名规范
const (
	KeyPrefixUser      = "user:"          // user:123
	KeyPrefixSession   = "session:"       // session:abc123
	KeyPrefixCache     = "cache:"         // cache:user:list
	KeyPrefixLock      = "lock:"          // lock:order:456
	KeyPrefixCounter   = "counter:"       // counter:page:views
)

// 使用函数生成 Key
func UserCacheKey(userId int64) string {
	return fmt.Sprintf("%s%d", KeyPrefixUser, userId)
}

func SessionKey(sessionId string) string {
	return fmt.Sprintf("%s%s", KeyPrefixSession, sessionId)
}

🔍 故障排查

1. 连接失败

问题: dial tcp xxx:6379: i/o timeout

排查步骤:

# 1. 检查 Redis 服务
kubectl get pods -n juwan | grep redis

# 2. 检查 Service
kubectl get svc -n juwan | grep redis

# 3. 测试网络连通性
kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \
  nc -zv user-redis-master 6379

# 4. 查看应用日志
kubectl logs -f user-rpc-xxx -n juwan

解决方案:

  • 确认 Host 配置正确
  • 确认网络策略允许访问
  • 检查 Redis Pod 状态

2. 认证失败

问题: NOAUTH Authentication required

查:

// 打印配置(调试用)
func main() {
	var c config.Config
	conf.MustLoad(*configFile, &c)
	
	// 检查密码是否正确加载
	fmt.Printf("Redis Config: Host=%s, Pass=%s\n", c.Redis.Host, c.Redis.Pass)
}

解决方案:

  • 确认环境变量 REDIS_PASSWORD 已设置
  • 确认 Secret 正确挂载
  • 检查密码是否正确

3. 性能问题

慢查询检测:

import "time"

func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) {
	start := time.Now()
	defer func() {
		duration := time.Since(start)
		if duration > 100*time.Millisecond {
			l.Logger.Warnf("Slow Redis query: %v", duration)
		}
	}()
	
	// 执行查询
	key := fmt.Sprintf("user:%d", userId)
	cached, err := l.svcCtx.Redis.Get(key)
	// ...
}

常见原因:

  • 连接池耗尽 → 增大 MaxActive
  • 大 Value 传输 → 拆分或压缩数据
  • 网络延迟 → 检查网络质量

4. 内存泄漏

检查连接是否正确关闭:

// go-zero 的 Redis 客户端会自动管理连接
// 但如果使用原生 go-redis 客户端,需要手动关闭

// ❌ 错误示例
func bad() {
	rdb := redis.NewClient(&redis.Options{...})
	// 使用完后没有关闭
}

// ✅ 正确示例
func good() {
	rdb := redis.NewClient(&redis.Options{...})
	defer rdb.Close()
	// ...
}

📖 最佳实践

1. 环境变量管理

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-rpc
  namespace: juwan
spec:
  template:
    spec:
      containers:
      - name: user-rpc
        image: user-rpc:v1
        env:
          # 从 Secret 读取 Redis 密码
          - name: REDIS_PASSWORD
            valueFrom:
              secretKeyRef:
                name: user-redis
                key: password
          
          # 从 ConfigMap 读取其他配置
          - name: REDIS_HOST
            valueFrom:
              configMapKeyRef:
                name: user-rpc-config
                key: redis.host

2. 配置分离

开发、测试、生产环境分离:

# 开发环境
go run usercenter.go -f etc/pb-dev.yaml

# 测试环境
go run usercenter.go -f etc/pb-test.yaml

# 生产环境
./usercenter -f etc/pb-prod.yaml

3. 监控指标

import (
	"github.com/zeromicro/go-zero/core/metric"
	"github.com/zeromicro/go-zero/core/prometheus"
)

var (
	redisCacheHit = metric.NewCounterVec(&metric.CounterVecOpts{
		Namespace: "user_rpc",
		Subsystem: "redis",
		Name:      "cache_hit_total",
		Help:      "redis cache hit count",
		Labels:    []string{"key"},
	})
	
	redisCacheMiss = metric.NewCounterVec(&metric.CounterVecOpts{
		Namespace: "user_rpc",
		Subsystem: "redis",
		Name:      "cache_miss_total",
		Help:      "redis cache miss count",
		Labels:    []string{"key"},
	})
)

func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) {
	cacheKey := fmt.Sprintf("user:%d", userId)
	
	cached, err := l.svcCtx.Redis.Get(cacheKey)
	if err == nil && cached != "" {
		redisCacheHit.Inc("user")
		var user User
		json.Unmarshal([]byte(cached), &user)
		return &user, nil
	}
	
	redisCacheMiss.Inc("user")
	// 查询数据库...
}

4. 错误处理

func (l *UserLogic) GetUser(userId int64) (*User, error) {
	cacheKey := fmt.Sprintf("user:%d", userId)
	
	// 缓存查询失败不应该中断流程
	cached, err := l.svcCtx.Redis.Get(cacheKey)
	if err == nil && cached != "" {
		var user User
		if json.Unmarshal([]byte(cached), &user) == nil {
			return &user, nil
		}
	}
	
	// 缓存失败,降级查询数据库
	user, err := l.getUserFromDB(userId)
	if err != nil {
		return nil, err
	}
	
	// 尝试回写缓存,失败不影响返回结果
	go func() {
		userJSON, _ := json.Marshal(user)
		if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil {
			l.Logger.Errorf("Failed to set cache: %v", err)
		}
	}()
	
	return user, nil
}

5. 缓存更新策略

Write-Through(同步更新):

func (l *UserLogic) UpdateUser(user *User) error {
	// 1. 更新数据库
	if err := l.updateUserInDB(user); err != nil {
		return err
	}
	
	// 2. 同步更新缓存
	cacheKey := fmt.Sprintf("user:%d", user.Id)
	userJSON, _ := json.Marshal(user)
	if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil {
		l.Logger.Errorf("Failed to update cache: %v", err)
	}
	
	return nil
}

Write-Behind(异步更新):

func (l *UserLogic) UpdateUserAsync(user *User) error {
	// 1. 立即更新缓存
	cacheKey := fmt.Sprintf("user:%d", user.Id)
	userJSON, _ := json.Marshal(user)
	l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600)
	
	// 2. 异步更新数据库
	go func() {
		if err := l.updateUserInDB(user); err != nil {
			l.Logger.Errorf("Failed to update DB: %v", err)
			// 回滚缓存
			l.svcCtx.Redis.Del(cacheKey)
		}
	}()
	
	return nil
}

📚 参考资源

官方文档

示例代码

相关工具


📝 总结

快速开始检查清单

  • 1. 在 config.go 中定义 Redis redis.RedisConf
  • 2. 在配置文件中添加 Redis 配置
  • 3. 在 ServiceContext 中初始化 redis.MustNewRedis(c.Redis)
  • 4. 在 Kubernetes中配置环境变量 REDIS_PASSWORD
  • 5. 在 logic 中使用 l.svcCtx.Redis
  • 6. 测试连接是否正常

生产环境推荐配置

Redis:
  Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379
  Type: sentinel
  Pass: ${REDIS_PASSWORD}
  MasterName: mymaster
  MaxIdle: 16
  MaxActive: 100
  ConnectTimeout: 5000
  ReadTimeout: 3000
  WriteTimeout: 3000

关键点提醒

  1. 生产环境必须使用 Sentinel 模式
  2. 密码通过环境变量传递,不要硬编码
  3. 合理设置过期时间,防止缓存雪崩
  4. 使用 Pipeline 优化批量操作
  5. 实现缓存降级策略,Redis 故障不影响主流程

文档版本: 1.0
创建日期: 2026年2月22日
维护者: Backend Team
下次审查: 2026年3月22日