前情提要
为了鼓励大家学习英语的积极性(划掉),脑子一热做了个排行榜鼓励大家内卷(并不)。
设计是当用户完成一课时的时候,给用户完成 count++
, 把卷王都放在排行榜里让大家谴责。
先叠个 buff,以下内容仅仅代表个人观点,我还是个菜鸡,第一次写文章,大佬们轻喷
为什么使用 redis 而不是 mysql
- 性能与效率: Redis是一个基于内存的数据存储系统,读写速度比 mysql 快很多,对于需要频繁更新和访问的排行榜数据来说非常重要。
- 数据结构优势 :Redis 的
ZSET
(有序集合)是一个非常适合排行榜的数据结构。它可以保持元素的排序,每个元素都关联一个分数(score),这使得更新排名和检索排名列表变得非常高效。在 MySQL 中实现类似的功能,可能需要复杂的查询和额外的逻辑来处理排名。 - 扩展性:Redis 具有良好的水平扩展性。随着用户数量的增长和访问量的增加,Redis 能够更容易地扩展以处理更大的负载。而 MySQL 在处理大量并发读写操作时可能会面临性能瓶颈。
- 简化复杂性:使用 Redis 可以简化应用程序的复杂性,特别是在需要处理实时排行榜的场景中。Redis 提供的操作通常比 SQL 查询更直观简单,对于开发者来说更易于理解和实现。
- 成本效益:虽然 Redis 需要占用更多的内存资源,但考虑到它提供的性能优势,这通常是一个值得的投资。相比之下,优化 MySQL 以处理高性能的排行榜可能需要更多的开发和维护成本。
Nest 如何连接 Redis
安装第三方库
bash
pnpm add @nestjs-modules/ioredis ioredis
如何使用
- 在
app.module.ts
添加以下代码
typescript
import { Module } from '@nestjs/common';
import { RedisModule } from '@nestjs-modules/ioredis';
import { AppController } from './app.controller';
@Module({
imports: [
RedisModule.forRoot({
type: 'single',
url: 'redis://localhost:6379',
}),
],
controllers: [AppController],
})
export class AppModule {}
2.在需要用到的 service
里使用 @InjectRedis
注入
typescript
import Redis from 'ioredis';
import { Controller, Get } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
@Controller()
export class AppController {
constructor(
@InjectRedis() private readonly redis: Redis,
) {}
@Get()
async getHello() {
await this.redis.set('key', 'Redis data!');
const redisData = await this.redis.get("key");
return { redisData };
}
}
就可以快乐使用 redis 了
下面我们需要简单的来了解以下 ZSET
便于理解后续的代码
ZSET
commands
-
ZADD: 为ZSET添加一个或多个给定分数的成员 或者说用一个或多个成员初始化ZSET
-
ZREM: 从 ZSET 中删除一个已存在成员
-
ZRANGE: 按排序顺序从 ZSET 中获取所有成员
-
ZRANGEBYSCORE: 根据分数范围提取 ZSET 中的成员
-
ZCOUNT: 以 ZSET 为单位,返回分数在提供范围内的成员数量
-
ZRANK:根据成员在 ZSET 中的得分,返回改成员的位置
-
ZSCORE:返回 ZSET 中成员的分数
-
ZINCRBY: 返回ZSET 中某个成员的分数
examples
ZADD
为 ZSET 指定 key 添加 items
bash
ZADD history 70 xiaoming 80 xiaoli 90 xiaobai
## output
127.0.0.1:6379> ZADD history 70 xiaoming 80 xiaoli 90 xiaobai
(integer) 3
ZRANGEBYSCORE
从 ZSET 指定 key 中获取范围内的分数
bash
ZRANGEBYSCORE history 0 100
## output
127.0.0.1:6379> ZRANGEBYSCORE history 0 100
1) "xiaoming"
2) "xiaoli"
3) "xiaobai"
ZRANGE
从 ZSET 指定 key 中获取范围内的items 升序
bash
ZRANGE history 0 10 WITHSCORES
## output
127.0.0.1:6379> ZRANGE history 0 10 WITHSCORES
1) 70 "xiaoming"
2) 80 "xiaoli"
3) 90 "xiaobai"
ZREVRANGE
ZRANGE的倒序版本
bash
ZREVRANGE history 0 10 WITHSCORES
## output
127.0.0.1:6379> ZREVRANGE history 0 10 WITHSCORES
1) "xiaobai"
2) "90"
3) "xiaoli"
4) "80"
5) "xiaoming"
6) "70"
ZSCORE
从 ZSET 指定 key 中获取 item 分数
bash
ZSCORE history xiaoli
## output
127.0.0.1:6379> ZSCORE history xiaoli
"80"
ZCOUNT
从 ZSET 指定 key 中获取范围内的人数
bash
## 获取所有
ZCOUNT history -inf +inf
## output
127.0.0.1:6379> ZCOUNT history -inf +inf
(integer) 3
## 获取指定
ZCOUNT history 0 80
## output
127.0.0.1:6379> ZCOUNT history 0 80
(integer) 2
ZRANK
从 ZSET 指定 key 中获取item 分数排名
bash
## output
127.0.0.1:6379> ZRANK history xiaoli
(integer) 1
127.0.0.1:6379> ZRANK history xiaoming
(integer) 0
这里可以看出来 排名是从0
开始的
ZINCBY
从 ZSET 指定 key 中添加 item 的分数
bash
ZINCRBY history 5 xiaoming
## output
127.0.0.1:6379> ZINCRBY history 5 xiaoming
"75"
ZREM
从 ZSET 指定 key 中 删除一个 item
bash
ZREM history xiaoli
## output
127.0.0.1:6379> ZREM history xiaoli
(integer) 1
127.0.0.1:6379> ZCOUNT history -inf +inf
(integer) 2
下面我们来讲讲如何实现
如何实现
先看整体的 service
文件
typescript
import { InjectRedis } from '@nestjs-modules/ioredis';
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { UserEntity } from '../user/user.decorators';
@Injectable()
export class RankService {
private readonly FINISH_COUNT_KEY = `user:finishCount`;
constructor(@InjectRedis() private readonly redis: Redis) {}
async userFinishCourse(userId: number, username: string) {
const member = `${userId}-${username}`;
let count = await this.redis.zscore(this.FINISH_COUNT_KEY, member);
if (!count) {
await this.redis.zadd(this.FINISH_COUNT_KEY, 1, member);
} else {
await this.redis.zincrby(this.FINISH_COUNT_KEY, 1, member);
}
count = await this.redis.zscore(this.FINISH_COUNT_KEY, member);
return count;
}
private getUserName(member: string) {
return member.split('-')[1];
}
private translateList(rankList: string[]) {
let res = [];
for (let i = 0; i < rankList.length; i += 2) {
let username = this.getUserName(rankList[i]);
let count = rankList[i + 1];
res.push({ username, count });
}
return res;
}
// return top 10 and self rank
async getRankList(user: UserEntity) {
// return [member, count, member, count, ...]
let rankList = await this.redis.zrevrange(
this.FINISH_COUNT_KEY,
0,
9,
'WITHSCORES',
);
let self = null;
if (user) {
const userRank = await this.redis.zrevrank(
this.FINISH_COUNT_KEY,
`${user.userId}-${user.username}`,
);
const userCount =
(await this.redis.zscore(
this.FINISH_COUNT_KEY,
`${user.userId}-${user.username}`,
)) ?? 0;
self = { username: user.username, count: userCount, rank: userRank };
}
return {
list: this.translateList(rankList),
self,
};
}
}
关键方法就是 userFinishCourse
和 getRankList
userFinishCourse
在这个方法中,我们把${userId}-${username}
当做用户的标识(不考虑更改用户名的情况下),先去 redis
中查找这个标识有没有这条数据,如果没有就记录,有的话则 count++
getRankList
这里因为我们需要卷王在最上面 所以得使用 ZREVRANK
来获取降序的数据, 可以看出ZSET
也是从下标0
开始的 这里我们需要更多或者更少数据调整9
就好了,剩下的就是对一些数据的处理,屏幕前的各位大佬看一眼就懂了
实现效果
最后
感谢崔哥让我参与到开源中,让我这个菜鸡学到不少东西,感谢催学社各位大佬对小弟的指点,感谢各位看官大佬。
欢迎大家来 pr 来体验这个简单模式学英语 的项目: github