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 {}
}
601

被折叠的 条评论
为什么被折叠?



