1. sorted set的简单介绍
参考链接:https://mp.weixin.qq.com/s/srkd73bS2n3mjIADLVg72A
Redis的Sorted Set(有序集合)是一种数据结构,它是一个不重复的字符串集合,每个元素都有一个对应的分数(score),可以根据分数对元素进行排序。Sorted Set的特点是能够在O(log(N))的时间复杂度内进行插入和删除操作,同时可以通过分数快速检索和排序元素。
具备以下特性:
唯一性:每个元素在集合中是唯一的,但可以有相同的分数。
排序:元素根据分数进行排序,分数相同的元素按字典序排序。
范围查询:支持通过分数或排名进行范围查询。
高效操作:对元素的插入、删除和查找操作均为O(log(N))。
在Redis中,数据结构的底层实现是非常关键的。对于你提到的Set和Sorted Set,它们的底层实现是不同的。
1.1. 底层结构介绍
Set
Redis的Set(无序集合)底层使用的是哈希表(Hash Table)。具体来说,Redis在实现Set时,使用了一个哈希表来存储集合中的元素。因为哈希表具有O(1)的时间复杂度来进行插入、删除和查找操作,所以Set在这些操作上非常高效。
Sorted Set
Redis的Sorted Set(有序集合)则是一个更复杂的数据结构,底层实现结合了两种结构:
- 哈希表:用于存储元素和其分数之间的映射关系。
- 跳表(Skip List):用于维护元素的有序性,以便能够高效地进行范围查询和排名操作。跳表是一种可以在O(log(N))时间复杂度内进行插入、删除和查找操作的数据结构。
总结
- Set:底层实现是哈希表。
- Sorted Set:底层实现是哈希表结合跳表。
这种设计使得Sorted Set能够在保证元素唯一性的同时,同时高效地支持按分数排序和范围查询等操作。这样,Redis可以在需要高效检索和排序的业务场景中,提供良好的性能表现。
1.2. 常用命令
# 将元素member添加到有序集合key中,如果元素已存在,则更新其分数为score。
ZADD Key score member
# 比如向排行榜leaderboard新添加三名玩家player_xxx, 分数如下所示:
ZADD leaderboard 100 "player_10086"
ZADD leaderboard 200 "player_10087"
ZADD leaderboard 150 "player_10088"# 返回[start,stop]范围内的集合成员,后面的选项可以决定分数也返回。
ZRANGE key start stop [WITHSCORES]
# 比如返回排在前两位的玩家
ZRANGE leaderboard 0 1 WITHSCORES
#结果输出
player_10086 100 player_10087 150# 如果先按分数高的在前面,也就是返回分数前两名的玩家,可以使用
ZRERANGE:该命令与ZRANGE一样格式,只不过它是倒序; ZSCORE Key member # 获取指定成员的分数
ZSCORE leadergroup "player_10086"
输出:100ZREM key member [member ...] #删除元素
ZREM leaderboard "player_10087"ZRANK key member #获取指定元素的排名
ZRANK leaderboard "player_10086"ZRANGEBYSCORE key min max [WITHSCORES] #按照分数范围查询
ZRANGEBYSCORE leaderboard 50 150 WITHSCORESZINCRBY Key score member #给元素member增加score分数
ZINCRBY leaderboard 40 "player_10086"
2. 常见的业务场景介绍
2.1. 排行榜系统
场景
排行榜系统:Sorted Set类型非常适合实现排行榜系统,如游戏得分排行榜、文章热度排行榜等。在一个在线游戏中,玩家的得分需要实时更新并显示在排行榜上。使用Sorted Set可以方便地根据得分高低进行排序。
优势
实时排序:根据玩家的得分自动排序,无需额外的排序操作。
动态更新:可以快速地添加新玩家或更新现有玩家的得分。
范围查询:方便地查询排行榜的前N名玩家。
解决方案
使用Redis Sorted Set来存储和管理游戏玩家的得分排行榜。
代码实现
package mainimport ("context""fmt""github.com/go-redis/redis/v8""log"
)var ctx = context.Background()// Redis 客户端初始化
var rdb = redis.NewClient(&redis.Options{Addr: "", // Redis 服务器地址Password: "", // 密码DB: 0, // 使用默认 DB
})// 更新玩家得分
func updatePlayerScore(playerID string, score float64) error {sortedSetKey := "playerScores"// 添加或更新玩家得分_, err := rdb.ZAdd(ctx, sortedSetKey, &redis.Z{Score: score, Member: playerID}).Result()return err
}// 获取排行榜
func getLeaderboard(start int, stop int) ([]string, error) {sortedSetKey := "playerScores"// 获取排行榜数据leaderboard, err := rdb.ZRangeWithScores(ctx, sortedSetKey, int64(start), int64(stop)).Result()if err != nil {return nil, err}var result []stringfor _, entry := range leaderboard {result = append(result, fmt.Sprintf("%s: %.2f", entry.Member.(string), entry.Score))}return result, nil
}// 获取前N名玩家
func getTopNPlayers(n int) ([]string, error) {return getLeaderboard(0, n-1) // 获取前N名,stop需要是n-1
}// 清理测试数据
func clearTestKeys() error {sortedSetKey := "playerScores"_, err := rdb.Del(ctx, sortedSetKey).Result()return err
}func main() {// 更新玩家得分示例if err := updatePlayerScore("player1", 100); err != nil {log.Fatalf("Error updating player score: %v", err)}if err := updatePlayerScore("player2", 200); err != nil {log.Fatalf("Error updating player score: %v", err)}if err := updatePlayerScore("player3", 150); err != nil {log.Fatalf("Error updating player score: %v", err)}// 获取前2名玩家的排行榜topPlayers, err := getTopNPlayers(2)if err != nil {log.Fatalf("Error getting top players: %v", err)}fmt.Println("Top 2 Players:")for _, player := range topPlayers {fmt.Println(player)}// 清理测试数据if err := clearTestKeys(); err != nil {log.Fatalf("Error clearing test keys: %v", err)}fmt.Println("Test keys cleared.")
}
2.2. 实时数据获取
场景
实时数据统计:Sorted Set可以用于实时数据统计,如网站的访问量统计、商品的销量统计等。在一个电商平台中,需要统计商品的销量,并根据销量对商品进行排序展示。
优势
自动排序:根据销量自动对商品进行排序。
灵活统计:可以按时间段统计销量,如每日、每周等。
解决方案
使用Redis Sorted Set来实现商品的销量统计和排序。
代码实现
package mainimport ("context""fmt""github.com/go-redis/redis/v8""log""time"
)var ctx = context.Background()// Redis 客户端初始化
var rdb = redis.NewClient(&redis.Options{Addr: "", // Redis 服务器地址Password: "", // 密码DB: 0, // 使用默认 DB
})// 更新商品销量
func updateProductSales(productID string, sales int64) {today := time.Now().Format("2006-01-02")sortedSetKey := "productSales:" + today// 增加商品销量rdb.ZIncrBy(ctx, sortedSetKey, float64(sales), productID)
}// 获取商品销量排行
func getProductSalesRanking(date string) ([]string, error) {sortedSetKey := "productSales:" + date// 获取销量排行数据ranking, err := rdb.ZRevRangeWithScores(ctx, sortedSetKey, 0, -1).Result() // 按销量从高到低排序if err != nil {return nil, err}var result []stringfor _, entry := range ranking {result = append(result, fmt.Sprintf("%s: %d", entry.Member.(string), int(entry.Score)))}return result, nil
}// 获取某个时间段的商品销量(如每日、每周)
func getSalesByPeriod(productID string, startDate string, endDate string) (int64, error) {totalSales := int64(0)start, _ := time.Parse("2006-01-02", startDate)end, _ := time.Parse("2006-01-02", endDate)for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {dateStr := d.Format("2006-01-02")sales, err := rdb.ZScore(ctx, "productSales:"+dateStr, productID).Result()if err == nil {totalSales += int64(sales)} else if err != redis.Nil {return 0, err // 其他错误}}return totalSales, nil
}func main() {// 示例:更新产品销量updateProductSales("product1", 10)updateProductSales("product2", 20)updateProductSales("product1", 5)// 示例:获取今日的产品销量排行today := time.Now().Format("2006-01-02")ranking, err := getProductSalesRanking(today)if err != nil {log.Fatalf("Error getting sales ranking: %v", err)}fmt.Println("Today's product sales ranking:")for _, entry := range ranking {fmt.Println(entry)}// 示例:获取某段时间内某个产品的总销量totalSales, err := getSalesByPeriod("product1", "2023-10-01", "2023-10-07")if err != nil {log.Fatalf("Error getting sales by period: %v", err)}fmt.Printf("Total sales for product1 from 2023-10-01 to 2023-10-07: %d\n", totalSales)// 清理测试数据rdb.Del(ctx, "productSales:"+today)// 如果需要清理特定日期的销量数据,可以在这里添加更多的 DEL 语句// rdb.Del(ctx, "productSales:2023-10-01")// rdb.Del(ctx, "productSales:2023-10-02")// 根据需求添加更多日期
}
注意事项:
- Sorted Set中的分数可以是浮点数,这使得它可以用于更精确的排序需求。
- 元素的分数可以动态更新,但应注意更新操作的性能影响。
- 使用Sorted Set进行范围查询时,应注意合理设计分数的分配策略,以避免性能瓶颈。
- 在设计排行榜或其他需要排序的功能时,应考虑数据的时效性和更新频率,选择合适的数据结构和索引策略。