redis基本数据结构-sorted set

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. 常用命令

bash 复制代码
# 将元素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来存储和管理游戏玩家的得分排行榜。

代码实现

go 复制代码
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来实现商品的销量统计和排序。

代码实现

go 复制代码
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进行范围查询时,应注意合理设计分数的分配策略,以避免性能瓶颈。
  • 在设计排行榜或其他需要排序的功能时,应考虑数据的时效性和更新频率,选择合适的数据结构和索引策略。
相关推荐
搬码后生仔10 分钟前
SQLite 是一个轻量级的嵌入式数据库,不需要安装服务器,直接使用文件即可。
数据库·sqlite
码农君莫笑11 分钟前
Blazor项目中使用EF读写 SQLite 数据库
linux·数据库·sqlite·c#·.netcore·人机交互·visual studio
江上挽风&sty13 分钟前
【Django篇】--动手实践Django基础知识
数据库·django·sqlite
向阳121817 分钟前
mybatis 动态 SQL
数据库·sql·mybatis
胡图蛋.18 分钟前
什么是事务
数据库
小黄人软件21 分钟前
20241220流水的日报 mysql的between可以用于字符串 sql 所有老日期的,保留最新日期
数据库·sql·mysql
张声录126 分钟前
【ETCD】【实操篇(三)】【ETCDCTL】如何向集群中写入数据
数据库·chrome·etcd
无为之士32 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
小汤猿人类1 小时前
open Feign 连接池(性能提升)
数据库
用户0099383143011 小时前
代码随想录算法训练营第十三天 | 二叉树part01
数据结构·算法