介绍
Redis 是一款高性能的键值对存储系统,提供了丰富的数据结构,其中包括有序集合(Sorted Set)。有序集合是一种基于哈希表和跳跃表实现的散列表,它可以存储一组带有分数的元素,并且可以按照分数从小到大或者从大到小的顺序排列元素,同时每个元素都有唯一的键值。
有序集合是一个非常实用的数据结构,它可以用来存储排行榜、社交网络关系、优先级队列等需要有序排列的数据。本文将详细讲解 Redis 中的有序集合的使用方法和底层实现原理。
基本操作
有序集合的基本操作包括添加元素、删除元素、更新元素分数、获取元素排名和分数等。下面分别介绍这些操作的使用方法。
添加元素
添加元素的操作可以使用 ZADD 命令,命令格式为:
ZADD key score member [score member ...]
其中,key 是有序集合的键值,score 是元素的分数,member 是元素的键值。可以通过多次指定 score 和 member 参数添加多个元素:
redis> ZADD myset 1 "one" (integer) 1 redis> ZADD myset 2 "two" 3 "three" (integer) 2
删除元素
删除元素的操作可以使用 ZREM 命令,命令格式为:
ZREM key member [member ...]
其中,key 是有序集合的键值,member 是要删除的元素的键值。可以同时删除多个元素:
redis> ZREM myset "one" "two" (integer) 2
更新元素分数
更新元素分数的操作可以使用 ZINCRBY 命令,命令格式为:
ZINCRBY key increment member
其中,key 是有序集合的键值,increment 是要增加的分数(可以是负数),member 是要更新的元素的键值。如果元素不存在,会自动添加并设置初始分数为 increment:
redis> ZADD myset 1 "one" (integer) 1 redis> ZINCRBY myset 2 "one" "3"
获取元素排名
获取元素排名的操作可以使用 ZRANK 命令和 ZREVRANK 命令,命令格式分别为:
ZRANK key member ZREVRANK key member
其中,key 是有序集合的键值,member 是要查询排名的元素的键值。ZRANK 命令返回元素在有序集合中从小到大排列的排名(排名从 0 开始计数),ZREVRANK 命令返回元素在有序集合中从大到小排列的排名。
redis> ZADD myset 20 "one" 30 "two" 10 "three" (integer) 3 redis> ZRANK myset "two" (integer) 2 redis> ZREVRANK myset "two" (integer) 0
获取元素分数
获取元素分数的操作可以使用 ZSCORE 命令,命令格式为:
ZSCORE key member
其中,key 是有序集合的键值,member 是要查询分数的元素的键值。如果元素不存在,返回 nil。
redis> ZADD myset 20 "one" 30 "two" 10 "three" (integer) 3 redis> ZSCORE myset "two" "30"
底层实现原理
有序集合的底层实现基于哈希表和跳跃表两种数据结构。
哈希表负责元素的键值到元素本身的映射,每个元素对应着一个哈希表的键值对。为了支持元素的分数排序,每次添加元素的同时会将元素键值和分数作为参数,将它们以字典序递增的方式拼接成字符串,然后将该字符串作为哈希表的键值,元素本身作为哈希表的值。
跳跃表负责元素的分数排序和排名计算,每个元素对应着一个跳跃表的节点。跳跃表是一种基于多层链表的数据结构,每一层都是一个有序的单链表,每个节点包括若干个指向下一层节点的指针。在查询元素排名时,可以从跳跃表的最高层开始,沿着对应元素分数递减的路径遍历跳跃表,直到找到对应元素的节点,统计该节点之前的元素个数即可得到排名。
示例代码
下面是一个使用有序集合的示例程序,它模拟从数亿条日志中提取前 10 名的关键词排名。假设每个日志的格式如下:
2022-01-01 08:00:00 INFO [http-nio-8080-exec-10] com.example.MyController - search keyword="apple" time=100ms
程序通过正则表达式解析每个日志中的关键词和查询时间,并使用有序集合记录每个关键词的查询次数和平均查询时间。最后,程序从有序集合中查询前 10 名关键词及其排名、查询次数和平均查询时间,输出到控制台。
// www.javascriptcn.com code example
const redis = require("redis");
const client = redis.createClient();
const log_regex = /keyword="(.*?)" time=(\d+)ms/;
// 解析日志并更新有序集合的元素分数和值
function update_word_stats(log) {
const match = log_regex.exec(log);
if (!match) return;
const word = match[1];
const time = parseInt(match[2]);
client.zincrby("word_rank", 1, word); // 自增查询次数
client.zincrby("word_time", time, word); // 自增查询时间
}
// 输出前 10 名关键词及其排名、查询次数和平均查询时间
function print_word_rank() {
client.zrevrange("word_rank", 0, 9, "withscores", function(err, reply) {
if (err) throw err;
console.log("Keyword Rank:");
let rank = 1;
for (let i = 0; i < reply.length; i += 2) {
const word = reply[i];
const count = parseInt(reply[i + 1]);
client.zscore("word_time", word, function(err, reply) {
if (err) throw err;
const time = parseInt(reply) / count;
console.log(`${rank}. ${word} (${count} times, ${time.toFixed(2)}ms/time)`);
rank++;
});
}
});
}
// 读取日志文件并逐行解析
const readline = require("readline");
const fs = require("fs");
const rl = readline.createInterface({
input: fs.createReadStream("logs.txt")
});
rl.on("line", update_word_stats);
rl.on("close", print_word_rank);在使用该示例程序前,需要先创建一个名为 word_rank 的有序集合和一个名为 word_time 的有序集合:
redis-cli> ZADD word_rank 0 redis-cli> ZADD word_time 0
启动程序后,它会从 logs.txt 文件中读取日志并更新有序集合。当所有日志都读取完成后,它会从有序集合中查询前 10 名关键词并输出到控制台。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/67d7e64fa941bf7134e209fc