node中使用redis有序集合实现排行榜
场景
想做一个用户排行榜,传统的实现方式是通过sort+skip筛选出来,但数据量大了效率会很低,然后了解到redis适合这类场景,就试着用redis实现。因为我也是刚接触redis,不熟悉,百度在node使用redis的相关文章比较少,redis驱动的文档也不是很详细,所以遇到了一些问题。这篇文章主要还是分享一下自己的思路,希望对您有帮助。
Redis 有序集合(sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。
redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。 相关文档
使用API:
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZREVRANGE key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到低
因为查的资料都是用了ZADD,但驱动的文档只写了这个:
Redis Commands
There is built-in support for all of the out-of-the-box Redis commands. They are exposed using the raw Redis command names (HSET, HGETALL, etc.) and a friendlier camel-cased version (hSet, hGetAll, etc.):
// raw Redis commands
await client.HSET('key', 'field', 'value');
await client.HGETALL('key');
// friendly JavaScript commands
await client.hSet('key', 'field', 'value');
await client.hGetAll('key');因为不知道怎么传参,然后我就直接百度搜 node redis zadd,找了好几篇文章,发现照他们的方式调用都会报参数错误,例:
/**
* zadd 有序集合插入 score
* @param {String} key
* @param {Number} score
* @param {String} value
* @param {Number} expire - 秒
*/
static async zadd(key, score, value, expire) {
if (!expire) {
return await redis.zadd(key, score, value);
}
return await redis.multi().zadd(key, score, value).expire(key, expire).exec();
}
————————————————
版权声明:本文为CSDN博主「AdleyTales」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/adley_app/article/details/885324897. 有序集合(SortedSets)
1. 常⽤操作: zadd(设置元素), zrange(获取范围内的元素),zrank(获取指定元素的排名,从0开始), zscore(获取指定元素的score,⽤户指定
的score)
client.zadd(['zdb',0,'mysql', 1,'mongo',2,'redis'], function(err, data) {
console.log(data); // 3
})
————————————————
版权声明:本文为CSDN博主「超级大超越」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012632105/article/details/124843611//zset
命令 行为 返回值 使用示例(略去回调函数)
zadd 将一个带有给定分支的成员添加到有序集合中 zadd('zset-key', score, 'key') (score为int)
原文链接:https://www.shuzhiduo.com/A/ke5jy2yaJr/没办法只好自己看代码,首先看说明:
ZADD(key: RedisCommandArgument, members: ZMember | ZMember[], options?: ZAddOptions | undefined): Promise<number>第一个参数好理解,key,关键在第二个参数,根据这里的类型和原本命令的格式,还有看别人之前调的代码,应该是[ score, member ],上代码:
// 签到实现的方法 省略其他业务代码
async function signIn(){
let uid = 1; // 这里假设有一个uid为1的用户
let val = await client.ZADD('rank:user:signIn',[1, uid]); // client 为redis客户端 这里省略了连接的步骤
console.log(val);
}
signIn();结果报错:UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'toString' of undefined
没办法只好继续往后面找,
// ZADD.d.ts
import { RedisCommandArgument, RedisCommandArguments } from '.';
import { ZMember } from './generic-transformers';
export declare const FIRST_KEY_INDEX = 1;
interface NX {
NX?: true;
}
interface XX {
XX?: true;
}
interface LT {
LT?: true;
}
interface GT {
GT?: true;
}
interface CH {
CH?: true;
}
interface INCR {
INCR?: true;
}
declare type ZAddOptions = (NX | (XX & LT & GT)) & CH & INCR;
export declare function transformArguments(key: RedisCommandArgument, members: ZMember | Array<ZMember>, options?: ZAddOptions): RedisCommandArguments;
export { transformNumberInfinityReply as transformReply } from './generic-transformers';
可以看到ZADD的第二个参数可以接收两种类型,members: ZMember | Array<ZMember>,
然后找到ZMember,可以看到ZMember是一个对象接口,里面有score和value属性
export interface ZMember {
score: number;
value: RedisCommandArgument;
}然后修改代码,可以看到代码提示出来了,这次应该对了。

// 签到实现的方法 省略其他业务代码
async function signIn(){
let uid = 1; // 这里假设有一个uid为1的用户
let val = await client.ZADD('rank:user:signIn',{
score: 1,
value: uid
}); // client 为redis客户端 这里省略了连接客户端的步骤
console.log(val);
}
signIn();结果又报错:UnhandledPromiseRejectionWarning: TypeError: Invalid argument type
觉得很奇怪,然后又去看文档,注意到:Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
成员类型必须为string,然后修改代码:
// 签到实现的方法 省略其他业务代码
async function signIn(){
let uid = 1; // 这里假设有一个uid为1的用户
let val = await client.ZADD('rank:user:signIn',{
score: 1,
value: String(uid)
}); // client 为redis客户端 这里省略了连接客户端的步骤
console.log(val);
}
signIn();最后执行成功。
原本的逻辑是在签到的实现里,用ZADD更新用户的签到总数,因为找的文章都是用这个API实现的,我也没多想,
写这篇文章的时候又想到,用ZADD需要知道用户之前的签到次数,在我只知道UID的情况下,又去查一遍会降低效率,
相当mongodb里有$inc自增操作符,那类似的redis里应该也有,然后找到了相关的API:
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
代码:
// 签到实现的方法 省略其他业务代码
async function signIn() {
let uid = 1; // 这里假设有一个uid为1的用户
let val = await client.ZINCRBY('rank:user:signIn', 1, String(uid)) // 用户每次签到更新总签到次数
console.log(val);
}
signIn();查询部分用到API:ZREVRANGE key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到低
Node的redis驱动并没有封装这个API,根据文档:
Unsupported Redis Commands
If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use .sendCommand():
await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']查询排行榜代码:
// 获取用户排行榜 省略其他业务代码
async function getSignInRank() {
let list = await client.sendCommand(['ZREVRANGE', 'rank:user:signIn', '0', '9'])
// 这里取出前十位 下标从0开始,包含9,需要注意这里的下标必须用字符串,不然报错
// list为根据签到总数排序的用户uid数组,
// 后面根据uid从数据库取数用户数据,这里就不写了。
}最后整理一下实现思路:
需要做什么排行榜,就在会更新这个数据的地方将数据保存进redis。
如需要做用户签到排行榜,就在签到的接口用ZINCRBY让用户的签到总数自增1;文章点赞排行榜则在点赞的接口保存数据。
具我了解redis可能会丢数据,所以最好定时将redis里的数据存进其他数据库。
以上都是我这段时间对redis的稍微学习总结出来的内容,因为并没有了解太多,所以可能会有地方理解的不对,因为后面没有用到redis,所以暂时不打算深入学习。