Redis有序集合(zset)介绍及实现排行榜

Redis有序集合(zset)

Redis 的有序集合(Sorted Set)是一种特殊的数据结构,它结合了集合和有序列表的特点,以下是对其的详细介绍:

一、数据结构特点

1. 元素的唯一性与有序性
  • 有序集合中的每个元素都是唯一的,就像普通集合一样。但是,与普通集合不同的是,有序集合中的元素是按照一个特定的分数(score)进行排序的。
  • 这个分数可以是整数或浮点数,用于确定元素在集合中的位置。分数越小,元素在集合中的位置越靠前;分数越大,元素在集合中的位置越靠后。
2. 高效的存储和查询
  • Redis 对有序集合的存储和查询操作进行了高度优化,使得在大规模数据下也能保持高效的性能。
  • 插入、删除和查找操作的时间复杂度通常为 O (log (N)),其中 N 是有序集合中的元素数量。
3. 范围查询
  • 可以方便地进行范围查询,即获取分数在某个范围内的元素。例如,可以查询分数大于等于某个值且小于等于另一个值的所有元素。
  • 这种范围查询在很多场景下非常有用,比如获取排行榜中的某个区间的元素。

二、应用场景

1. 排行榜系统
  • 这是有序集合最常见的应用场景之一。例如,可以将用户的得分作为分数,用户 ID 作为元素存储在有序集合中,从而轻松实现用户排行榜。
  • 在你的提供的代码中,就用有序集合来存储商品的销量排行榜,商品的总销量作为分数,商品 ID 作为元素,通过这种方式可以快速获取销量排名靠前的商品。
  • 可以方便地更新用户得分,当用户的得分发生变化时,只需要更新对应元素的分数即可,Redis 会自动调整元素在有序集合中的位置。
2. 时间序列数据存储
  • 如果将时间戳作为分数,可以将有序集合用于存储时间序列数据。例如,可以存储传感器的读数、日志事件等按时间顺序排列的数据。
  • 这样可以方便地进行时间范围查询,获取特定时间段内的数据。
3. 优先级队列
  • 可以将任务的优先级作为分数,任务的标识符作为元素,实现优先级队列。高优先级的任务会排在前面,可以更快地被处理。
  • 当有新任务加入时,根据其优先级将其插入到有序集合中。当需要处理任务时,从有序集合中取出分数最小(即优先级最高)的元素进行处理。

三、操作方法

1. 添加元素
  • 使用ZADD命令可以向有序集合中添加一个或多个元素,并为每个元素指定一个分数。例如:redisClient.ZAdd(ctx, key, &redis.Z{Score: score, Member: member})。
  • 如果元素已经存在于有序集合中,ZADD命令会更新该元素的分数,并相应地调整元素在有序集合中的位置。
2. 获取元素
  • 使用ZRANGE或ZREVRANGE命令可以获取有序集合中的元素。ZRANGE按照分数从小到大的顺序返回元素,而ZREVRANGE按照分数从大到小的顺序返回元素。
  • 可以指定返回元素的范围,例如获取前 N 个元素:redisClient.ZRevRangeWithScores(ctx, key, 0, int64(n - 1)).Result(),这在排行榜等场景中非常有用。
3. 删除元素
  • 使用ZREM命令可以从有序集合中删除一个或多个元素。例如:redisClient.ZRem(ctx, key, member1, member2,…)。
4. 范围查询
  • 使用ZRANGEBYSCORE或ZREVRANGEBYSCORE命令可以进行范围查询,获取分数在某个范围内的元素。例如:redisClient.ZRangeByScoreWithScores(ctx, key, &redis.ZRangeBy{Min: minScore, Max: maxScore}).Result()。
5. 获取元素的分数
  • 使用ZSCORE命令可以获取某个元素在有序集合中的分数。例如:redisClient.ZScore(ctx, key, member).Result()。

场景需求

思路及步骤:

用redis实现条件为昨日销量的榜单,每天晚上1:00定时读取mysql数据库中的商品订单数据 并展示榜单
1.每天1:00同步数据(定时任务)
从mysql数据库中查询昨日的商品订单,
将计算好的昨日数据存储到redis有序集合中
设置过期时间为一天
2.redis获取排序后的昨日销量列表

示例代码:
package main

import (
    "context"
    "fmt"
    "log"
    "strconv"
    "sync"
    "time"

    "github.com/go-redis/redis/v8"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var ctx = context.Background()
var redisClient *redis.Client
var db *gorm.DB
var mu sync.Mutex

// Product 商品结构体
type Product struct {
    gorm.Model
    Name  string  `gorm:"comment:商品名称"`
    Price float64 `gorm:"comment:商品价格"`
    Sales int     `gorm:"comment:商品销量"`
}

// Order 订单结构体
type Order struct {
    gorm.Model
    ProductID string `gorm:"comment:商品ID"`
    Quantity  int    `gorm:"comment:商品数量"`
    OrderDate string `gorm:"comment:订单日期"`
}

// 从 MySQL 获取昨日订单数据(按时间戳查询)
func getYesterdayOrdersFromMySQL() (map[string]int, error) {
    // 加互斥锁,确保在该函数执行期间数据的一致性
    mu.Lock()
    defer mu.Unlock()

    // 获取昨天的日期
    yesterday := time.Now().AddDate(0, 0, -1)
    // 获取昨天凌晨的时间戳
    startTimestamp := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location()).Unix()
    // 获取今天凌晨的时间戳
    endTimestamp := time.Date(yesterday.AddDate(0, 0, 1).Year(), yesterday.AddDate(0, 0, 1).Month(), yesterday.AddDate(0, 0, 1).Day(), 0, 0, 0, 0, yesterday.Location()).Unix()
    log.Printf("查询时间范围:从 %d 到 %d", startTimestamp, endTimestamp)
    log.Println("开始查询 MySQL 数据库")

    // 定义一个切片用于存储查询结果
    var orders []Order
    // 使用 Gorm 查询数据库表
    result := db.Model(&Order{}).
       Select("product_id, SUM(quantity) as quantity").
       Where("order_date >=? AND order_date <?", startTimestamp, endTimestamp).
       Group("product_id").
       Find(&orders)
    if result.Error != nil {
       // 如果查询过程中有错误,打印错误信息并返回错误
       log.Println("从 MySQL 获取昨日销售数据失败:", result.Error)
       return nil, result.Error
    }
    log.Printf("查询到 %d 条数据", len(orders))
    log.Println("查询 MySQL 数据库完成")

    // 创建一个映射用于存储商品 ID 和总数量
    salesData := make(map[string]int)
    for _, order := range orders {
       // 将每个商品的总数量存储到映射中
       salesData[order.ProductID] = order.Quantity
    }

    return salesData, nil
}

// 更新昨日销量榜单异步处理函数
func updateYesterdaySalesListAsync() {
    for {
       // 这里可以处理实时订单数据更新,如果你有实时订单数据流入的话
       time.Sleep(time.Second)
    }
}

// 从数据库更新昨日销量榜单到 Redis,并设置一天后过期
func updateYesterdaySalesListFromDatabase() {
    log.Println("开始更新 Redis 榜单")
    // 从 MySQL 获取昨日订单数据
    yesterdaySalesData, err := getYesterdayOrdersFromMySQL()
    if err != nil {
       log.Println("从 MySQL 获取昨日销售数据失败:", err)
       return
    }
    // 创建临时键
    tempKey := "yesterday_sales_list_temp"
    // 删除原有的榜单键和临时键
    redisClient.Del(ctx, "yesterday_sales_list")
    redisClient.Del(ctx, tempKey)
    for productID, salesQuantity := range yesterdaySalesData {
       // 将商品 ID 和总数量添加到 Redis 的临时键中
       redisClient.ZAdd(ctx, tempKey, &redis.Z{
          Score:  float64(salesQuantity),
          Member: productID,
       })
    }
    // 将临时键重命名为正式的榜单键
    redisClient.Rename(ctx, tempKey, "yesterday_sales_list")
    // 设置榜单一天后过期
    redisClient.Expire(ctx, "yesterday_sales_list", time.Hour*24)
    log.Println("更新 Redis 榜单完成")
}

// 获取昨日销量前 N 的商品函数
func getTopNYesterdaySales(n int) []Product {
    results, err := redisClient.ZRevRangeWithScores(ctx, "yesterday_sales_list", 0, int64(n-1)).Result()
    if err != nil {
       log.Println("查询榜单数据失败:", err)
       return nil
    }
    topNProducts := make([]Product, len(results))
    for i, item := range results {
       productID := item.Member.(string)
       productIDInt, _ := strconv.Atoi(productID)
       // 假设商品详细信息存储在哈希结构中,键为 product_info
       productInfo, err := redisClient.HGetAll(ctx, "product_info_"+productID).Result()
       if err != nil {
          log.Println("查询商品详细信息失败:", err)
          continue
       }
       price, _ := strconv.ParseFloat(productInfo["price"], 64)
       sales, _ := strconv.Atoi(item.Member.(string))
       // 根据新的商品结构体创建商品对象
       topNProducts[i] = Product{
          Model: gorm.Model{ID: uint(productIDInt)},
          Name:  productInfo["name"],
          Price: price,
          Sales: sales,
       }
    }
    return topNProducts
}

func main() {
    // 连接 Redis
    redisClient = redis.NewClient(&redis.Options{
       Addr:     "localhost:6379",
       Password: "",
       DB:       0,
    })

    // 连接 MySQL 使用 Gorm
    var err error
    dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
    db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
       log.Fatal("连接数据库失败:", err)
    }

    // 创建数据库表(如果不存在)
    err = db.AutoMigrate(&Product{}, &Order{})
    if err != nil {
       log.Fatal("创建数据库表失败:", err)
    }

    // 启动异步更新任务
    go updateYesterdaySalesListAsync()

    // 每天 15:17 执行从数据库更新榜单任务
    go func() {
       for {
          now := time.Now()
          targetTime := time.Date(now.Year(), now.Month(), now.Day(), 1, 0, 0, 0, now.Location())
          if now.Before(targetTime) {
             log.Println("等待到今天的 1:00")
             time.Sleep(targetTime.Sub(now))
          } else {
             targetTime = targetTime.AddDate(0, 0, 1)
             log.Println("等待到明天的 1:00")
             time.Sleep(targetTime.Sub(now))
          }
          updateYesterdaySalesListFromDatabase()
       }
    }()

    // 展示榜单
    topNProducts := getTopNYesterdaySales(5)
    for _, product := range topNProducts {
       fmt.Println(product)
    }

    // 主程序运行,阻塞主线程
    select {}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值