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(有序集合)则是一个更复杂的数据结构,底层实现结合了两种结构:

  1. 哈希表:用于存储元素和其分数之间的映射关系。
  2. 跳表(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进行范围查询时,应注意合理设计分数的分配策略,以避免性能瓶颈。
  • 在设计排行榜或其他需要排序的功能时,应考虑数据的时效性和更新频率,选择合适的数据结构和索引策略。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部