node中使用redis有序集合实现排行榜

四糸乃赛高 Lv.5 冰封大地
34 2022/7/9笔记
场景

想做一个用户排行榜,传统的实现方式是通过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 (HSETHGETALL, etc.) and a friendlier camel-cased version (hSethGetAll, 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/88532489
7. 有序集合(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;
}

然后修改代码,可以看到代码提示出来了,这次应该对了。

https://www.hualigs.cn/image/62c8654332907.jpg

// 签到实现的方法 省略其他业务代码
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,所以暂时不打算深入学习。

# Nodejs # Redis

评论

后参与评论
还没有评论,来说点什么吧~