redis有序集合写入和求交集的速度

背景

团队小伙伴做了一个需求。大概的需求是有很多的图片作品,图片作品有一些类别,每个人进入到每个类别的作品业,根据权重优先查看权重最高的的作品,权重大概是基于每个人对该作品的浏览计算,浏览过的作品放在最后展示。小伙伴是基于redis的有序集合的方式实现的,主要用到写入和求交集这两个操作。我们这个系统的作品数量不算多,目前只有5000左右,用户也只有5000左右,这么小的数据量用这种方式实现,我原本觉得是没什么问题的。但是,实际测试下来,就很离谱,不知道他的程序里哪里导致的异常耗时。出于好奇,我写了一段测试代码,测试一下不同数量级的数据,写入和求交集的时长。

测试程序

Go 复制代码
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"time"

	"github.com/redis/go-redis/v9"
)

// BatchZAdd function to batch insert elements into a Redis sorted set
func BatchZAdd(ctx context.Context, rdb *redis.Client, key string, members []redis.Z) error {
	// Pipeline to batch the ZADD commands
	pipe := rdb.Pipeline()

	if err := pipe.ZAdd(ctx, key, members...).Err(); err != nil {
		return fmt.Errorf("failed to add member to sorted set: %v", err)
	}

	// Execute the pipeline
	_, err := pipe.Exec(ctx)
	if err != nil {
		return fmt.Errorf("failed to execute pipeline: %v", err)
	}

	return nil
}

func main() {
	// Create a new Redis client
	rdb := redis.NewClient(&redis.Options{
		Addr:     "10.10.37.100:6379", // Replace with your Redis server address
		Password: "dreame@2020",
		DB:       11,
	})

	// Define the context for the Redis operation
	ctx := context.Background()

	startTime := time.Now()
	// Define the key for the sorted set
	key := "mySortedSet|test1"
	// Prepare the members to be added to the sorted set
	members := make([]redis.Z, 1000000)
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < len(members); i++ {
		// Generate a random score and member for each element in the set
		randomNumber := rand.Intn(50)
		members[i] = redis.Z{Score: float64(randomNumber), Member: fmt.Sprintf("member%d", i)}
	}

	// Call the BatchZAdd function to batch insert the members
	if err := BatchZAdd(ctx, rdb, key, members); err != nil {
		log.Fatalf("failed to batch insert members into sorted set: %v", err)
	}
	fmt.Println("mySortedSet|test1写入序列执行时间:", time.Since(startTime).Seconds(), "s")

	// 写入第二个序列
	startTime = time.Now()
	key2 := "mySortedSet|test2"
	members2 := make([]redis.Z, 1000000)
	for i := 0; i < len(members2); i++ {
		// Generate a random score and member for each element in the set
		randomNumber := rand.Intn(50)
		members2[i] = redis.Z{Score: float64(randomNumber), Member: fmt.Sprintf("member%d", i)}
	}

	// Call the BatchZAdd function to batch insert the members
	if err := BatchZAdd(ctx, rdb, key2, members2); err != nil {
		log.Fatalf("failed to batch insert members into sorted set: %v", err)
	}
	fmt.Println("mySortedSet|test2写入序列执行时间:", time.Since(startTime).Seconds(), "s")
	log.Println("Members successfully added to the sorted set")

	destinationKey := "mySortedSet|destination"

	cmd := rdb.ZInterStore(ctx, destinationKey, &redis.ZStore{
		Keys:      []string{key, key2},
		Aggregate: "MIN",
	})
	if cmd.Err() != nil {
		log.Fatalf("failed to create intersection of sorted sets: %v", cmd.Err())
	}

	startTime = time.Now()
	nums := int64(0)
	var err1 error
	timeout := time.After(10 * time.Second) // 设置超时为5秒
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-timeout:
			fmt.Println("Timeout reached, exiting loop.", time.Now())
			return
		case <-ticker.C:
			cmd = rdb.ZCard(ctx, destinationKey)
			nums, err1 = cmd.Result()
			if err1 != nil {
				fmt.Println("Error getting ZCard:", err1)
				return // 或者其他错误处理逻辑
			}
			fmt.Println("*********************nums:*************", nums)
			if nums != 0 {
				fmt.Println("执行时间:", time.Since(startTime).Seconds(), "s")
				log.Println("Members successfully ZInterStore the sorted set")
				return // 退出循环
			}
		}
	}
}

这种写法和测试程序中的方法相比,100万一下的数据,时间稍微长了一点。但是,测试程序中的方法在100万的数据写入时就会报错了,但是,图中的方法不会报错。

| | 写入时长(单位:s) | 求交集时长 |
| 5千 | 0.02 | 0.014 |
| 1万 | 0.03 | 0.026 |
| 10万 | 0.19 | 0.012 |
| 100万 | 5.22 | 0.015 |

1000万

我们发现,求交集的时间都还好。但是,写入时长是呈线性增长,实际执行1000万数据写入,报了超时错误,也可以理解,毕竟时间呈线性增长,如果没有超时限制,应该也需要一分钟左右。因为其写入时间复杂度是O(log(N)) for each item added, where N is the number of elements in the sorted set。而求交集的时间复杂度是O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.

思考

出了上文使用管道(Pipeline)的方式,还有更优的写入方案么?最先冒出来的想法是并发写入,但是,我想到了一个问题,由于有序集合涉及到排序,并发写,是否会有锁竞争的问题?甚至排序会出现问题?我们可以实际测试一下看看结果如何。

Go 复制代码
func ConcurrentBatchZAdd(ctx context.Context, rdb *redis.Client, key string, members []redis.Z, numGoroutines int) error {
	var wg sync.WaitGroup
	batchSize := len(members) / numGoroutines

	for i := 0; i < numGoroutines; i++ {
		start := i * batchSize
		end := start + batchSize
		if i == numGoroutines-1 {
			end = len(members) // 最后一个 goroutine 处理剩余的成员
		}

		wg.Add(1)
		go func(members []redis.Z) {
			defer wg.Done()
			pipe := rdb.Pipeline()
			cmd := pipe.ZAdd(ctx, key, members...)
			_, err := pipe.Exec(ctx)
			if err != nil {
				log.Printf("failed to execute pipeline: %v", err)
			}
			if err := cmd.Err(); err != nil {
				log.Printf("failed to batch insert members into sorted set: %v", err)
			}
		}(members[start:end])
	}

	wg.Wait() // 等待所有 goroutine 完成
	return nil
}

10万及以下的数据,似乎效率没有差异。到了100万的情况,似乎有了差异,并发写100万的时长是2.3s。1000万的数据,并发写入时会报超时错误,按照我的理解,并发写入其实并不能提升写入效率。

相关推荐
问道飞鱼10 分钟前
【Springboot知识】Springboot结合redis实现分布式锁
spring boot·redis·分布式
Yeats_Liao10 分钟前
Spring 框架:配置缓存管理器、注解参数与过期时间
java·spring·缓存
Elastic 中国社区官方博客23 分钟前
使用 Elasticsearch 导航检索增强生成图表
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
小金的学习笔记27 分钟前
RedisTemplate和Redisson的使用和区别
数据库·redis·缓存
取址执行30 分钟前
Redis发布订阅
java·redis·bootstrap
新知图书42 分钟前
MySQL用户授权、收回权限与查看权限
数据库·mysql·安全
文城5211 小时前
Mysql存储过程(学习自用)
数据库·学习·mysql
沉默的煎蛋1 小时前
MyBatis 注解开发详解
java·数据库·mysql·算法·mybatis
呼啦啦啦啦啦啦啦啦1 小时前
【Redis】事务
数据库·redis·缓存
HaoHao_0101 小时前
AWS Serverless Application Repository
服务器·数据库·云计算·aws·云服务器