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"
输出:100
ZREM key member [member ...] #删除元素
ZREM leaderboard "player_10087"
ZRANK key member #获取指定元素的排名
ZRANK leaderboard "player_10086"
ZRANGEBYSCORE key min max [WITHSCORES] #按照分数范围查询
ZRANGEBYSCORE leaderboard 50 150 WITHSCORES
ZINCRBY Key score member #给元素member增加score分数
ZINCRBY leaderboard 40 "player_10086"
2. 常见的业务场景介绍
2.1. 排行榜系统
场景
排行榜系统:Sorted Set类型非常适合实现排行榜系统,如游戏得分排行榜、文章热度排行榜等。在一个在线游戏中,玩家的得分需要实时更新并显示在排行榜上。使用Sorted Set可以方便地根据得分高低进行排序。
优势
实时排序:根据玩家的得分自动排序,无需额外的排序操作。
动态更新:可以快速地添加新玩家或更新现有玩家的得分。
范围查询:方便地查询排行榜的前N名玩家。
解决方案
使用Redis Sorted Set来存储和管理游戏玩家的得分排行榜。
代码实现
package main
import (
"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 []string
for _, 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 main
import (
"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 []string
for _, 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进行范围查询时,应注意合理设计分数的分配策略,以避免性能瓶颈。
- 在设计排行榜或其他需要排序的功能时,应考虑数据的时效性和更新频率,选择合适的数据结构和索引策略。