Redis学习之go-redis

一、连接管理

1. 基础连接

go

go 复制代码
import "github.com/redis/go-redis/v9"

// 单机连接
rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "", // 无密码
    DB:       0,  // 默认DB
})

// 集群连接
rdb := redis.NewClusterClient(&redis.ClusterOptions{
    Addrs: []string{":7000", ":7001", ":7002"},
})

2. 连接池配置

go

go 复制代码
rdb := redis.NewClient(&redis.Options{
    PoolSize:     100,     // 连接池大小
    MinIdleConns: 10,      // 最小空闲连接
    MaxRetries:   3,       // 最大重试次数
    DialTimeout:  5 * time.Second,
    ReadTimeout:  3 * time.Second,
    WriteTimeout: 3 * time.Second,
    PoolTimeout:  4 * time.Second,
})

二、String 字符串

基本操作

go

复制代码
ctx := context.Background()

// SET/GET
rdb.Set(ctx, "key", "value", 0)
val, err := rdb.Get(ctx, "key").Result()

// 带过期时间
rdb.SetEx(ctx, "key", "value", 10*time.Second)

// 批量操作
rdb.MSet(ctx, "key1", "value1", "key2", "value2")
rdb.MGet(ctx, "key1", "key2")

// 自增/自减
rdb.Incr(ctx, "counter")
rdb.IncrBy(ctx, "counter", 5)

三、Hash 哈希表

go

go 复制代码
// HSET/HGET
rdb.HSet(ctx, "user:1000", "name", "John", "age", 30)
name := rdb.HGet(ctx, "user:1000", "name").Val()

// 获取所有字段
all := rdb.HGetAll(ctx, "user:1000").Val()

// HMSET/HMGET
rdb.HMSet(ctx, "user:1001", map[string]interface{}{
    "name": "Alice",
    "age":  25,
})

// 字段自增
rdb.HIncrBy(ctx, "user:1000", "age", 1)

// 删除字段
rdb.HDel(ctx, "user:1000", "age")

四、List 列表

go

go 复制代码
// LPUSH/RPUSH
rdb.LPush(ctx, "mylist", "value1", "value2")
rdb.RPush(ctx, "mylist", "value3")

// LPOP/RPOP
val := rdb.LPop(ctx, "mylist").Val()

// LRANGE
items := rdb.LRange(ctx, "mylist", 0, -1).Val()

// LTRIM 保留指定范围
rdb.LTrim(ctx, "mylist", 0, 9)

// 阻塞操作
result := rdb.BLPop(ctx, 5*time.Second, "mylist")

五、Set 集合

go

go 复制代码
// SADD/SMEMBERS
rdb.SAdd(ctx, "myset", "member1", "member2")
members := rdb.SMembers(ctx, "myset").Val()

// SISMEMBER
exists := rdb.SIsMember(ctx, "myset", "member1").Val()

// 集合运算
rdb.SUnion(ctx, "set1", "set2")      // 并集
rdb.SInter(ctx, "set1", "set2")      // 交集
rdb.SDiff(ctx, "set1", "set2")       // 差集

// SCARD 获取集合大小
size := rdb.SCard(ctx, "myset").Val()

六、Sorted Set 有序集合

go

go 复制代码
// ZADD
rdb.ZAdd(ctx, "leaderboard", redis.Z{
    Score:  100,
    Member: "player1",
})

// 批量添加
members := []redis.Z{
    {Score: 200, Member: "player2"},
    {Score: 150, Member: "player3"},
}
rdb.ZAdd(ctx, "leaderboard", members...)

// ZRANGE 按分数排序
rdb.ZRange(ctx, "leaderboard", 0, -1)           // 升序
rdb.ZRevRange(ctx, "leaderboard", 0, -1)        // 降序

// 带分数返回
results := rdb.ZRangeWithScores(ctx, "leaderboard", 0, -1).Val()

// ZRANK 获取排名
rank := rdb.ZRank(ctx, "leaderboard", "player1").Val()

// ZSCORE 获取分数
score := rdb.ZScore(ctx, "leaderboard", "player1").Val()

// ZINCRBY 增加分数
rdb.ZIncrBy(ctx, "leaderboard", 50, "player1")

// ZCOUNT 分数区间计数
count := rdb.ZCount(ctx, "leaderboard", "100", "200").Val()

七、Stream 流

go

go 复制代码
// XADD 添加消息
id, err := rdb.XAdd(ctx, &redis.XAddArgs{
    Stream: "mystream",
    ID:     "*",  // 自动生成ID
    Values: map[string]interface{}{
        "field1": "value1",
        "field2": "value2",
    },
}).Result()

// XREAD 读取消息
messages, err := rdb.XRead(ctx, &redis.XReadArgs{
    Streams: []string{"mystream", "0"},
    Count:   10,
    Block:   0,
}).Result()

// 消费者组
// 创建消费者组
rdb.XGroupCreate(ctx, "mystream", "mygroup", "0")

// 消费者读取
messages, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
    Group:    "mygroup",
    Consumer: "consumer1",
    Streams:  []string{"mystream", ">"},
    Count:    10,
    Block:    0,
}).Result()

// 确认消息
rdb.XAck(ctx, "mystream", "mygroup", id)

八、事务

go

go 复制代码
// MULTI/EXEC
pipe := rdb.TxPipeline()

pipe.Set(ctx, "key1", "value1", 0)
pipe.Incr(ctx, "counter")
pipe.Expire(ctx, "key1", time.Hour)

_, err := pipe.Exec(ctx)

// Watch 乐观锁
err := rdb.Watch(ctx, func(tx *redis.Tx) error {
    // 获取当前值
    val, err := tx.Get(ctx, "key").Int()
    if err != nil && err != redis.Nil {
        return err
    }
    
    // 事务开始
    _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
        pipe.Set(ctx, "key", val+1, 0)
        return nil
    })
    return err
}, "key")

九、发布订阅

go

go 复制代码
// 创建订阅者
pubsub := rdb.Subscribe(ctx, "mychannel")

// 接收消息
ch := pubsub.Channel()
for msg := range ch {
    fmt.Println(msg.Channel, msg.Payload)
}

// 发布消息
err := rdb.Publish(ctx, "mychannel", "hello").Err()

// 模式订阅
pubsub := rdb.PSubscribe(ctx, "news.*")

// 关闭订阅
pubsub.Close()

十、管道(Pipeline)

go

go 复制代码
pipe := rdb.Pipeline()

pipe.Set(ctx, "key1", "value1", 0)
pipe.Get(ctx, "key1")
pipe.Expire(ctx, "key1", time.Hour)

cmds, err := pipe.Exec(ctx)

十一、实用技巧

1. 错误处理

go

go 复制代码
val, err := rdb.Get(ctx, "key").Result()
if err == redis.Nil {
    fmt.Println("key does not exist")
} else if err != nil {
    panic(err)
}

2. Scan 命令处理大数据集

go

go 复制代码
var cursor uint64
for {
    var keys []string
    keys, cursor, err = rdb.Scan(ctx, cursor, "pattern:*", 100).Result()
    // 处理keys
    if cursor == 0 {
        break
    }
}

3. 连接健康检查

go 复制代码
err := rdb.Ping(ctx).Err()
if err != nil {
    // 处理连接错误
}

十二、案例讲解

go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"time"

	"github.com/redis/go-redis/v9"
)

// ============================ 数据结构定义 ============================

// GameRankingSystem 游戏排行榜系统结构体
// 封装了Redis客户端和上下文,提供统一的游戏服务接口
type GameRankingSystem struct {
	rdb *redis.Client  // Redis客户端实例,用于所有Redis操作
	ctx context.Context // 上下文,用于控制请求超时和取消
}

// User 用户结构体
// 表示游戏中的用户,包含用户的基本信息和资产
type User struct {
	ID       string // 用户唯一标识
	Username string // 用户名
	Level    int    // 用户等级
	Coins    int    // 用户金币数量
}

// ============================ 系统初始化 ============================

// NewGameRankingSystem 创建排行榜系统实例
// 返回值: *GameRankingSystem 初始化好的游戏系统实例
func NewGameRankingSystem() *GameRankingSystem {
	// 创建 Redis 客户端配置
	// Options结构体包含了连接Redis所需的所有配置参数
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis服务器地址
		Password: "",               // 密码,空字符串表示无密码
		DB:       0,                // 数据库编号,0表示默认数据库
		PoolSize: 20,               // 连接池大小,控制最大并发连接数
	})

	// 创建上下文,用于控制请求的生命周期
	ctx := context.Background()

	// 测试连接是否成功
	// Ping命令是Redis最简单的命令,用于测试连通性
	if err := rdb.Ping(ctx).Err(); err != nil {
		// 连接失败时,终止程序运行
		log.Fatalf("Redis连接失败: %v", err)
	}

	// 返回初始化好的系统实例
	return &GameRankingSystem{
		rdb: rdb,
		ctx: ctx,
	}
}

// ============================ String 操作示例 ============================

// StringOperations 演示String类型的基本操作
// String是最简单的Redis数据类型,常用于存储简单的键值对
func (g *GameRankingSystem) StringOperations() {
	fmt.Println("\n=== String 操作演示 ===")

	// 1. 存储用户会话信息(String)
	// 场景:用户登录后创建会话,30分钟后自动过期
	// 键名模式:session:{userID},便于管理和查找
	sessionKey := fmt.Sprintf("session:%s", "user123")
	
	// SetEx命令:设置键值对并指定过期时间
	// 参数说明:ctx上下文,key键名,value值,expiration过期时间
	g.rdb.SetEx(g.ctx, sessionKey, "logged_in", 30*time.Minute)
	fmt.Println("✅ 用户会话已设置,30分钟后过期")

	// 2. 每日登录奖励计数器
	// 场景:统计每天有多少用户登录,用于活动分析
	// 键名模式:daily:login:{date},按日期组织数据
	today := time.Now().Format("2006-01-02")
	dailyLoginKey := fmt.Sprintf("daily:login:%s", today)
	
	// Incr命令:将键存储的数字值增加1
	// 如果键不存在,则会先创建并设置为0,再执行+1操作
	count, _ := g.rdb.Incr(g.ctx, dailyLoginKey).Result()
	
	// Expire命令:设置键的过期时间
	// 这里设置48小时过期,保留2天的统计数据
	g.rdb.Expire(g.ctx, dailyLoginKey, 48*time.Hour)
	fmt.Printf("✅ 今日第 %d 位用户登录\n", count)

	// 3. 获取会话状态
	// 模拟用户访问时的会话验证
	status, err := g.rdb.Get(g.ctx, sessionKey).Result()
	if err == redis.Nil {
		// redis.Nil是特殊的错误类型,表示键不存在
		fmt.Println("❌ 会话不存在或已过期")
	} else if err != nil {
		// 其他错误类型,如网络错误、Redis服务错误等
		fmt.Printf("❌ 获取会话失败: %v\n", err)
	} else {
		// 获取成功,输出会话状态
		fmt.Printf("✅ 用户会话状态: %s\n", status)
	}
}

// ============================ Hash 操作示例 ============================

// HashOperations 演示Hash类型的基本操作
// Hash是field-value结构,适合存储对象
func (g *GameRankingSystem) HashOperations(user User) {
	fmt.Println("\n=== Hash 操作演示 ===")

	// 用户信息键名
	// 键名模式:user:{userID},使用Hash存储用户的所有属性
	userKey := fmt.Sprintf("user:%s", user.ID)

	// 1. 存储用户信息到 Hash(一个对象多个字段)
	// HSet命令:设置Hash中的字段值
	// 可以一次性设置多个字段,减少网络请求次数
	g.rdb.HSet(g.ctx, userKey,
		"username", user.Username,        // 字符串字段
		"level", user.Level,             // 数字字段
		"coins", user.Coins,             // 数字字段
		"last_login", time.Now().Format(time.RFC3339), // 时间字段
	)
	fmt.Println("✅ 用户信息已存储到 Hash")

	// 2. 获取用户特定字段
	// HGet命令:获取Hash中指定字段的值
	username, _ := g.rdb.HGet(g.ctx, userKey, "username").Result()
	coins, _ := g.rdb.HGet(g.ctx, userKey, "coins").Int()
	fmt.Printf("✅ 用户名: %s, 金币数: %d\n", username, coins)

	// 3. 增加用户金币(字段自增)
	// HIncrBy命令:将Hash中指定字段的值增加给定数值
	// 原子操作,适用于计数器和余额增减场景
	newCoins, _ := g.rdb.HIncrBy(g.ctx, userKey, "coins", 100).Result()
	fmt.Printf("✅ 获得100金币奖励,当前金币: %d\n", newCoins)

	// 4. 获取所有用户信息
	// HGetAll命令:获取Hash中所有字段和值
	// 返回map[string]string类型,适用于获取完整对象
	allInfo, _ := g.rdb.HGetAll(g.ctx, userKey).Result()
	fmt.Printf("✅ 用户完整信息: %v\n", allInfo)

	// 5. 检查字段是否存在
	// HExists命令:检查Hash中指定字段是否存在
	exists, _ := g.rdb.HExists(g.ctx, userKey, "level").Result()
	fmt.Printf("✅ level字段是否存在: %v\n", exists)
}

// ============================ Sorted Set 操作示例 ============================

// SortedSetOperations 演示Sorted Set类型的基本操作
// Sorted Set是有序集合,每个元素都有一个分数用于排序
func (g *GameRankingSystem) SortedSetOperations(users []User) {
	fmt.Println("\n=== Sorted Set 操作演示 ===")

	// 排行榜键名
	leaderboardKey := "leaderboard:global"

	// 1. 批量添加用户到排行榜(ZADD)
	// 准备要添加到Sorted Set的成员列表
	var members []redis.Z // redis.Z是Sorted Set成员的结构体
	for _, user := range users {
		// 计算用户分数
		// 分数设计:level * 1000 + coins
		// 这样高等级玩家总是排在低等级玩家前面,同等级玩家按金币数排序
		score := float64(user.Level*1000 + user.Coins)
		members = append(members, redis.Z{
			Score:  score,    // 排序分数
			Member: user.Username, // 成员标识
		})
	}

	// ZAdd命令:向Sorted Set添加成员
	// 如果成员已存在,则更新其分数
	g.rdb.ZAdd(g.ctx, leaderboardKey, members...)
	fmt.Println("✅ 用户排行榜已更新")

	// 2. 获取前3名玩家(ZREVRANGE)
	// 降序排列,分数高的在前面
	fmt.Println("\n🏆 排行榜前三名:")
	top3, _ := g.rdb.ZRevRangeWithScores(g.ctx, leaderboardKey, 0, 2).Result()
	for i, player := range top3 {
		fmt.Printf("第%d名: %s (分数: %.0f)\n", i+1, player.Member, player.Score)
	}

	// 3. 获取用户排名(ZREVRANK)
	// 获取成员在Sorted Set中的排名(从0开始)
	for _, user := range users {
		rank, err := g.rdb.ZRevRank(g.ctx, leaderboardKey, user.Username).Result()
		if err == redis.Nil {
			// 用户不在排行榜中
			fmt.Printf("❌ 用户 %s 不在排行榜中\n", user.Username)
		} else {
			// 排名从0开始,所以需要+1显示人类可读的排名
			fmt.Printf("👤 用户 %s 的排名: 第%d名\n", user.Username, rank+1)
		}
	}

	// 4. 获取分数段玩家数量(ZCOUNT)
	// 统计指定分数范围内的成员数量
	minScore := "5000"
	maxScore := "8000"
	count, _ := g.rdb.ZCount(g.ctx, leaderboardKey, minScore, maxScore).Result()
	fmt.Printf("📊 分数在 %s-%s 之间的玩家数: %d\n", minScore, maxScore, count)
}

// ============================ Set 操作示例 ============================

// SetOperations 演示Set类型的基本操作
// Set是无序集合,元素不重复,支持集合运算
func (g *GameRankingSystem) SetOperations(user User) {
	fmt.Println("\n=== Set 操作演示 ===")

	// 1. 用户好友集合
	// 键名模式:user:{userID}:friends
	friendsKey := fmt.Sprintf("user:%s:friends", user.ID)
	
	// SAdd命令:向Set添加成员
	// 如果成员已存在,则不会重复添加
	g.rdb.SAdd(g.ctx, friendsKey, "user456", "user789", "user999")
	fmt.Println("✅ 好友列表已添加")

	// 2. 获取所有好友
	// SMembers命令:获取Set中所有成员
	// 注意:Set是无序的,每次返回的顺序可能不同
	friends, _ := g.rdb.SMembers(g.ctx, friendsKey).Result()
	fmt.Printf("👥 %s 的好友列表: %v\n", user.Username, friends)

	// 3. 检查是否是好友
	// SIsMember命令:检查指定成员是否在Set中
	isFriend, _ := g.rdb.SIsMember(g.ctx, friendsKey, "user456").Result()
	fmt.Printf("❓ user456 是否是好友: %v\n", isFriend)

	// 4. 随机推荐好友(SRANDMEMBER)
	// SRandMember命令:从Set中随机返回一个成员
	// 可用于好友推荐、抽奖等场景
	randomFriend, _ := g.rdb.SRandMember(g.ctx, friendsKey).Result()
	fmt.Printf("🎲 随机推荐好友: %s\n", randomFriend)

	// 5. 好友数量
	// SCard命令:获取Set中成员的数量
	friendCount, _ := g.rdb.SCard(g.ctx, friendsKey).Result()
	fmt.Printf("📊 好友总数: %d\n", friendCount)
}

// ============================ List 操作示例 ============================

// ListOperations 演示List类型的基本操作
// List是有序列表,元素可以重复,支持从两端操作
func (g *GameRankingSystem) ListOperations(user User) {
	fmt.Println("\n=== List 操作演示 ===")

	// 1. 用户最近游戏记录(List作为队列)
	// 键名模式:user:{userID}:game_history
	// 使用List存储用户最近的游戏记录
	gameHistoryKey := fmt.Sprintf("user:%s:game_history", user.ID)
	
	// 添加最近的游戏记录
	// LPush命令:从列表左侧插入元素,最新记录在最前面
	for i := 1; i <= 5; i++ {
		gameInfo := fmt.Sprintf("游戏%d_得分%d_时间%s", 
			i, 
			rand.Intn(1000), // 随机生成得分
			time.Now().Format("15:04:05")) // 当前时间
		g.rdb.LPush(g.ctx, gameHistoryKey, gameInfo)
	}
	fmt.Println("✅ 游戏记录已保存")

	// 2. 获取最近3场游戏(LRANGE)
	// LRange命令:获取列表中指定范围的元素
	// 参数:0表示第一个元素,2表示第三个元素(包含)
	fmt.Println("\n🎮 最近3场游戏记录:")
	recentGames, _ := g.rdb.LRange(g.ctx, gameHistoryKey, 0, 2).Result()
	for i, game := range recentGames {
		fmt.Printf("  %d. %s\n", i+1, game)
	}

	// 3. 保持最近10条记录(LTRIM)
	// LTrim命令:修剪列表,只保留指定范围内的元素
	// 参数:0表示起始索引,9表示结束索引
	// 常用于实现固定大小的历史记录列表
	g.rdb.LTrim(g.ctx, gameHistoryKey, 0, 9)
	fmt.Println("✅ 已修剪,只保留最近10条记录")

	// 4. 获取列表长度
	// LLen命令:获取列表的长度
	historyLength, _ := g.rdb.LLen(g.ctx, gameHistoryKey).Result()
	fmt.Printf("📊 游戏历史记录总数: %d\n", historyLength)
}

// ============================ 事务操作示例 ============================

// TransactionOperations 演示事务操作
// Redis事务可以保证多个命令的原子性执行
func (g *GameRankingSystem) TransactionOperations(user1, user2 User) {
	fmt.Println("\n=== 事务操作演示 ===")

	// 场景:用户之间转账金币,需要保证原子性
	// 必须确保扣款和入账同时成功或同时失败

	// 用户信息键名
	user1Key := fmt.Sprintf("user:%s", user1.ID)
	user2Key := fmt.Sprintf("user:%s", user2.ID)
	transferAmount := 50 // 转账金额

	// 使用事务管道(TxPipeline)
	fmt.Printf("💰 开始转账: %s 给 %s 转账 %d 金币\n", 
		user1.Username, user2.Username, transferAmount)

	// 开始事务
	// Watch命令:监控指定的键,如果在事务执行期间被其他客户端修改,则事务失败
	// 这是Redis的乐观锁机制
	err := g.rdb.Watch(g.ctx, func(tx *redis.Tx) error {
		// 在Watch回调中,所有操作都必须通过tx对象执行
		
		// 获取双方当前金币数量
		coins1, err := tx.HGet(g.ctx, user1Key, "coins").Int()
		if err != nil && err != redis.Nil {
			return err
		}

		coins2, err := tx.HGet(g.ctx, user2Key, "coins").Int()
		if err != nil && err != redis.Nil {
			return err
		}

		fmt.Printf("转账前: %s=%d金币, %s=%d金币\n", 
			user1.Username, coins1, user2.Username, coins2)

		// 检查余额是否足够
		// 业务逻辑检查必须在事务中执行
		if coins1 < transferAmount {
			return fmt.Errorf("余额不足")
		}

		// 执行事务
		// TxPipelined命令:在事务中执行多个命令
		_, err = tx.TxPipelined(g.ctx, func(pipe redis.Pipeliner) error {
			// 扣减转出方金币
			// HIncrBy命令的负数参数表示减少
			pipe.HIncrBy(g.ctx, user1Key, "coins", int64(-transferAmount))
			
			// 增加转入方金币
			pipe.HIncrBy(g.ctx, user2Key, "coins", int64(transferAmount))
			
			// 记录转账日志(可选,用于审计)
			transferLog := fmt.Sprintf("transfer:%s:%s:%d", 
				user1.ID, user2.ID, time.Now().Unix())
			pipe.Set(g.ctx, transferLog, transferAmount, 24*time.Hour)
			
			return nil
		})
		return err
	}, user1Key, user2Key) // Watch 监视的键,这些键在事务期间不能改变

	// 处理事务结果
	if err != nil {
		fmt.Printf("❌ 转账失败: %v\n", err)
	} else {
		fmt.Println("✅ 转账成功!")

		// 验证转账结果
		newCoins1, _ := g.rdb.HGet(g.ctx, user1Key, "coins").Int()
		newCoins2, _ := g.rdb.HGet(g.ctx, user2Key, "coins").Int()
		fmt.Printf("转账后: %s=%d金币, %s=%d金币\n", 
			user1.Username, newCoins1, user2.Username, newCoins2)
	}
}

// ============================ 发布订阅示例 ============================

// PubSubOperations 演示发布订阅模式
// Redis Pub/Sub 是一种消息通信模式,发送者发送消息,订阅者接收消息
func (g *GameRankingSystem) PubSubOperations() {
	fmt.Println("\n=== 发布订阅演示 ===")

	channelName := "game:announcements" // 频道名称

	// 启动一个goroutine作为订阅者
	// 在实际应用中,订阅者通常是独立的服务或进程
	go func() {
		// Subscribe命令:订阅指定频道
		pubsub := g.rdb.Subscribe(g.ctx, channelName)
		// defer确保在函数退出时关闭订阅,释放资源
		defer pubsub.Close()

		// Channel方法:返回一个Go channel,用于接收消息
		ch := pubsub.Channel()

		fmt.Println("👂 订阅者已启动,等待消息...")
		
		// 循环接收消息
		for msg := range ch {
			// msg包含频道名称和消息内容
			fmt.Printf("📢 收到公告: %s\n", msg.Payload)
			
			// 收到特定消息后退出
			// 这是演示用的控制逻辑,实际应用中可能不需要
			if msg.Payload == "exit" {
				fmt.Println("⏹️ 订阅者退出")
				return
			}
		}
	}()

	// 给订阅者一点时间启动,确保订阅者已经连接
	// 在实际生产环境中,应该有更好的同步机制
	time.Sleep(100 * time.Millisecond)

	// 发布消息
	fmt.Println("\n📡 发布游戏公告...")
	
	// 模拟要发布的消息列表
	announcements := []string{
		"系统维护通知:今晚23:00-24:00进行维护",
		"新活动上线:周末双倍经验活动开始!",
		"恭喜玩家 '游戏高手' 获得全服首杀!",
		"exit", // 特殊消息,让订阅者退出
	}

	// 依次发布每条消息
	for _, msg := range announcements {
		// Publish命令:向指定频道发布消息
		// 所有订阅了该频道的客户端都会收到消息
		g.rdb.Publish(g.ctx, channelName, msg)
		
		// 模拟延迟,让输出更清晰
		time.Sleep(500 * time.Millisecond)
	}
}

// ============================ 批量操作示例 ============================

// PipelineOperations 演示Pipeline批量操作
// Pipeline可以将多个命令一次性发送给Redis,减少网络往返时间
func (g *GameRankingSystem) PipelineOperations(users []User) {
	fmt.Println("\n=== Pipeline 批量操作演示 ===")

	// 使用 Pipeline 批量执行命令,减少网络往返
	// Pipeline不是事务,不保证原子性,但能提高性能
	pipe := g.rdb.Pipeline()

	// 批量准备命令
	for _, user := range users {
		userKey := fmt.Sprintf("user:%s:stats", user.ID)
		
		// 批量设置命令
		// 这些命令会被缓存,不会立即发送到Redis
		pipe.HSet(g.ctx, userKey,
			"kills", rand.Intn(100),   // 随机生成击杀数
			"deaths", rand.Intn(50),   // 随机生成死亡数
			"assists", rand.Intn(30),  // 随机生成助攻数
		)
		
		// 设置过期时间
		pipe.Expire(g.ctx, userKey, time.Hour)
	}

	// 执行所有命令
	fmt.Println("🚀 正在批量执行命令...")
	start := time.Now()
	
	// Exec命令:一次性发送所有缓存的命令到Redis
	cmds, err := pipe.Exec(g.ctx)
	elapsed := time.Since(start)

	if err != nil {
		fmt.Printf("❌ Pipeline执行失败: %v\n", err)
		return
	}

	// 输出执行结果
	fmt.Printf("✅ 批量执行完成,处理了 %d 个用户\n", len(users))
	fmt.Printf("⏱️  总耗时: %v (使用普通命令需 %v 左右)\n", 
		elapsed, time.Duration(len(users)*2)*time.Millisecond)
	
	// cmds包含每个命令的执行结果,如果需要可以进一步处理
	_ = cmds // 这里不需要使用,所以用下划线忽略
}

// ============================ 清理数据 ============================

// Cleanup 清理测试数据
// 在生产环境中应该小心使用,这里是为了演示后清理测试数据
func (g *GameRankingSystem) Cleanup(patterns ...string) {
	fmt.Println("\n=== 清理测试数据 ===")
	
	// 遍历所有要清理的模式
	for _, pattern := range patterns {
		// 使用 SCAN 迭代删除,避免阻塞
		// SCAN命令比KEYS命令更安全,不会阻塞Redis服务
		var cursor uint64
		for {
			var foundKeys []string
			var err error
			
			// SCAN命令:迭代遍历匹配模式的键
			// 参数说明:cursor游标,pattern匹配模式,count每次返回的键数量
			foundKeys, cursor, err = g.rdb.Scan(g.ctx, cursor, pattern, 10).Result()
			if err != nil {
				break
			}

			// 如果找到匹配的键,删除它们
			if len(foundKeys) > 0 {
				// Del命令:删除一个或多个键
				g.rdb.Del(g.ctx, foundKeys...)
				fmt.Printf("🧹 已删除: %v\n", foundKeys)
			}

			// cursor为0表示迭代结束
			if cursor == 0 {
				break
			}
		}
	}
	fmt.Println("✅ 数据清理完成")
}

// ============================ 主函数 ============================

// main 程序入口函数
func main() {
	fmt.Println("🎮 Redis 游戏排行榜系统演示")
	fmt.Println("=" * 50)

	// 初始化系统
	// 创建GameRankingSystem实例,建立Redis连接
	gameSystem := NewGameRankingSystem()
	
	// defer确保程序退出时关闭Redis连接,释放资源
	defer func() {
		if err := gameSystem.rdb.Close(); err != nil {
			fmt.Printf("关闭Redis连接时出错: %v\n", err)
		}
	}()

	// 创建测试用户数据
	// 这些用户用于演示各种Redis操作
	users := []User{
		{ID: "1001", Username: "游戏高手", Level: 10, Coins: 5000},
		{ID: "1002", Username: "幸运玩家", Level: 8, Coins: 3000},
		{ID: "1003", Username: "新手玩家", Level: 3, Coins: 1000},
		{ID: "1004", Username: "氪金大佬", Level: 15, Coins: 10000},
	}

	// 初始化随机数种子
	// 确保每次运行生成的随机数不同
	rand.Seed(time.Now().UnixNano())

	// 执行各个功能演示
	// 按顺序演示Redis的各种数据结构和功能
	
	// 1. String操作演示
	gameSystem.StringOperations()
	
	// 2. Hash操作演示(使用第一个用户)
	gameSystem.HashOperations(users[0])
	
	// 3. Sorted Set操作演示(排行榜功能)
	gameSystem.SortedSetOperations(users)
	
	// 4. Set操作演示(好友功能)
	gameSystem.SetOperations(users[0])
	
	// 5. List操作演示(游戏历史记录)
	gameSystem.ListOperations(users[0])
	
	// 6. 事务操作演示(转账功能)
	gameSystem.TransactionOperations(users[0], users[1])
	
	// 7. Pipeline批量操作演示
	gameSystem.PipelineOperations(users)
	
	// 8. 发布订阅演示(需要异步执行)
	gameSystem.PubSubOperations()
	
	// 等待发布订阅演示完成
	// 因为发布订阅是异步的,需要等待足够的时间让消息被处理
	time.Sleep(3 * time.Second)

	// 清理测试数据
	// 删除演示过程中创建的所有测试数据,避免影响下次运行
	gameSystem.Cleanup(
		"user:*",           // 所有用户数据
		"session:*",        // 会话数据
		"daily:*",          // 每日统计数据
		"leaderboard:*",    // 排行榜数据
		"transfer:*",       // 转账日志数据
	)

	fmt.Println("\n🎉 演示完成!")
	fmt.Println("\n📚 总结:")
	fmt.Println("1. String: 适合简单键值对、计数器和缓存")
	fmt.Println("2. Hash: 适合存储对象和结构化数据")
	fmt.Println("3. Sorted Set: 适合排行榜和范围查询")
	fmt.Println("4. Set: 适合集合运算和不重复列表")
	fmt.Println("5. List: 适合队列和历史记录")
	fmt.Println("6. 事务: 适合需要原子性的操作")
	fmt.Println("7. Pipeline: 适合批量操作,提高性能")
	fmt.Println("8. Pub/Sub: 适合实时消息通知")
}

核心知识点总结:

  1. 连接管理最佳实践

    连接池配置:PoolSize 控制最大并发连接数,避免连接过多

    连接测试:使用 Ping() 验证连接有效性

    资源释放:使用 defer 确保连接关闭,防止资源泄漏

  2. 数据结构选择原则

    String:简单缓存、计数器、会话管理

    Hash:用户信息、商品信息等结构化数据

    Sorted Set:排行榜、带权重的队列

    Set:好友关系、标签系统、共同关注

    List:消息队列、最新动态、操作日志

  3. 事务 vs Pipeline

    事务(Transaction):保证原子性,适合转账、库存扣减

    流水线(Pipeline):提高性能,适合批量操作但不需原子性

  4. 性能优化技巧

    使用Pipeline:减少网络往返,批量操作提升性能

    合理设置过期时间:避免内存泄漏,自动清理过期数据

    使用SCAN替代KEYS:避免阻塞Redis服务

    连接池配置:根据并发量调整连接池大小

  5. 错误处理要点

    区分redis.Nil:键不存在的特殊错误,需单独处理

    事务失败处理:Watch失败时重试或回退

    连接错误:实现重连机制和降级策略

  6. 实际应用场景

    游戏系统:排行榜(Sorted Set)、用户信息(Hash)、好友(Set)

    电商系统:购物车(Hash)、商品库存(String)、订单队列(List)

    社交系统:关注关系(Set)、动态流(List)、消息通知(Pub/Sub)

相关推荐
bing.shao5 小时前
Golang WaitGroup 踩坑
开发语言·数据库·golang
石像鬼₧魂石5 小时前
Fail2Ban核心架构学习
linux·学习·ubuntu
逑之5 小时前
学习使用typora
学习
走在路上的菜鸟6 小时前
Android学Dart学习笔记第十三节 注解
android·笔记·学习·flutter
周杰伦_Jay6 小时前
【Go/Python/Java】基础语法+核心特性对比
java·python·golang
sszdlbw6 小时前
后端springboot框架入门学习--第一篇
java·spring boot·学习
秋深枫叶红6 小时前
嵌入式第三十五篇——linux系统编程——exec族函数
linux·前端·学习
不穿格子的程序员7 小时前
Redis篇4——Redis深度剖析:内存淘汰策略与缓存的三大“天坑”
数据库·redis·缓存·雪崩·内存淘汰策略
richxu202510017 小时前
嵌入式学习之路>单片机核心原理篇>(14) ARM 架构
arm开发·单片机·学习