NestJs: 定时任务+redis实现阅读量功能
抛个砖头
不知道大家用了这么久的掘金,有没有对它文章中的阅读量的实现有过好奇呢?
想象一下,你每次打开一篇文章时,都会有一个数字告诉你多少人已经读过这篇文章。那么这个数字是怎么得出来的呢?
有些人可能会认为,每次有人打开文章,数字就加1,不是很简单吗? 起初我也以为这样,哈哈(和大家站在同一个高度),但(好了,我不说了,继续往下看吧!)
引个玉
文章阅读量的统计看似简单,实则蕴含着巧妙的逻辑和高效的技术实现。我们想要得到的阅读量,并非简单的页面刷新次数,而是真正独立阅读过文章的人数。因此,传统的每次页面刷新加1的方法显然不够准确,它忽略了用户的重复访问。
同时,阅读作为一个高频操作,如果每次都直接写入数据库,无疑会给数据库带来巨大的压力,甚至可能影响到整个系统的性能和稳定性。这就需要我们寻找一种既能准确统计阅读量,又能减轻数据库压力的方法。
Redis,这个高性能的内存数据库,为我们提供了解决方案。我们可以利用Redis的键值对存储特性,将用户ID和文章ID的组合作为键,设置一个短暂的过期时间,比如15分钟。当用户首次访问文章时,我们在Redis中为这个键设置一个值,表示该用户已经阅读过这篇文章。如果用户在15分钟内再次访问,我们可以直接判断该键是否存在,如果存在,则不再增加阅读量,否则进行增加。
这种方法的优点在于,它能够准确地统计出真正阅读过文章的人数,而不是简单的页面刷新次数。同时,通过将阅读量先存储在Redis中,我们避免了频繁地写入数据库,从而大大减轻了数据库的压力。
最后,我们还需要考虑如何将Redis中的阅读量最终写入数据库。由于数据库的写入操作相对较重,我们不宜频繁进行。因此,我们可以选择在业务低峰期,比如凌晨2到4点,使用定时任务将Redis中的阅读量批量写入数据库。这样,既保证了阅读量的准确统计,又避免了频繁的数据库写入操作,实现了高效的系统运行。
思路梳理
- 😎Redis 助力阅读量统计,方法超好用!✨
- 🧐在 Redis 存用户和文章关系,轻松解决多次无效阅读!👏
- 💪定时任务来帮忙,Redis 数据写入数据库,不再
那么接下来就是实现环节
代码层面
项目使用的后端框架为NestJS
配置下redis
一、安装redis plugin
npm install --save redis
二、创建redis模块
三、初始化连接redis相关配置
@Module({
providers: [
RedisService,
{
provide: 'REDIS_CLIENT',
async useFactory(configService: ConfigService) {
console.log(configService.get('redis_server_host'));
const client = createClient({
socket: {
host: configService.get('redis_server_host'),
port: configService.get('redis_server_port'),
},
database: configService.get('redis_server_db'),
});
await client.connect();
return client;
},
inject: [ConfigService],
},
],
exports: [RedisService],
})
Redis是一个Key-Value型数据库,可以用作数据库,所有的数据以Key-Value的形式存在服务器的内存中,其中Value可以是多种数据结构,如字符串(String)、哈希(hashes)、列表(list)、集合(sets)和有序集合(sorted sets)等类型
在这里会用到字符串和哈希两种。
创建文章表和用户表
我的项目中创建有post.entity和user.entity这两个实体表,并为post文章表添加以下
这三个字段,在这里我们只拿 阅读量 说事。
访问文章详情接口-阅读量+1
/**
* @description 增加阅读量
* @param id
* @returns
*/
@Get('xxx/:id')
@RequireLogin()
async frontIncreViews(@Param('id') id: string, @Req() _req: any,) {
console.log('frontFindOne');
return await this.postService.frontIncreViews(+id, _req?.user);
}
前文已经说过,同一个用户多次刷新,如果不做处理,就会产生多次无效的阅读量。 因此,为了避免这种情况方式,我们需要为其增加一个用户文章id组合而成的标记,并设置在有效时间内不产生多次阅读量。
那么,有的掘友可能会产生一个疑问,如果用户未登录,那么以游客的身份去访问文章就不产生阅读记录了吗?
其实同理!
在我的项目中只是,要求需要用户登录后才能访问,
那么我这就会以 userID_postID_ 来组成标识区分用户和文章罢了。
而如果想以游客身份,我们可以获取用户 IP_postID 这样的组合来做标识即可
接下来说下postService
中调用的frontIncreViews
方法
直接贴代码:
const res = await this.redisService.hashGet(`post_${id}`);
if (res.viewCount === undefined) {
const post = await this.postRepository.findOne({ where: { id } });
post.viewCount++;
await this.postRepository.update(id, { viewCount: post.viewCount });
await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});
// 在用户访问文章的时候在 redis 存一个 10 分钟过期的标记,有这个标记的时候阅读量不增加
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return post.viewCount;
} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);
if (flag) {
return res.viewCount;
}
await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;
}
}
- 从Redis获取文章阅读量:
const res = await this.redisService.hashGet(`post_${id}`);
使用Redis的哈希表结构,从键post_${id}
中获取文章的信息,其中可能包含阅读量(viewCount
)、点赞数(likeCount
)和收藏数(collectCount
)。
2. 检查Redis中是否存在阅读量:
if (res.viewCount === undefined) {
如果Redis中没有阅读量数据,说明这篇文章的阅读量还没有被初始化。
3. 从数据库中获取文章并增加阅读量:
const post = await this.postRepository.findOne({ where: { id } });
post.viewCount++;
await this.postRepository.update(id, { viewCount: post.viewCount });
从数据库中获取文章,然后增加阅读量,并更新数据库中的文章阅读量。
4. 将更新后的文章信息存回Redis:
await this.redisService.hashSet(`post_${id}`, {
viewCount: post.viewCount,
likeCount: post.likeCount,
collectCount: post.collectCount,
});
将更新后的文章信息(包括新的阅读量、点赞数和收藏数)存回Redis的哈希表中。
5. 设置用户访问标记:
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
在用户访问文章时,在Redis中设置一个带有10分钟过期时间的标记,用于防止在10分钟内重复增加阅读量。
6. 返回阅读量:
return post.viewCount;
返回更新后的阅读量。
7. 如果Redis中存在阅读量:
} else {
const flag = await this.redisService.get(`user_${user.id}_post_${id}`);
console.log(flag);
如果Redis中存在阅读量数据,则检查用户是否已经访问过该文章。
8. 检查用户访问标记:
if (flag) {
return res.viewCount;
}
如果用户已经访问过该文章(标记存在),则直接返回当前阅读量,不增加。
9. 如果用户未访问过文章:
await this.redisService.hashSet(`post_${id}`, {
...res,
viewCount: +res.viewCount + 1,
});
await this.redisService.set(`user_${user.id}_post_${id}`, 1, 10);
return +res.viewCount + 1;
如果用户未访问过该文章,则增加阅读量,并重新设置用户访问标记。然后返回更新后的阅读量。
简而言之,目的是在用户访问文章时,确保文章阅读量只增加一次,即使用户在短时间内多次访问。
NestJS使用定时任务包,实现redis数据同步到数据库中
有的掘友可能疑问,既然已经用redis来做阅读量记录了,为什么还要同步到数据库中,前文开始的时候,就已经提到过了,一旦我们的项目重启, redis 数据就没了,而数据库却有着“数据持久性的优良品质”。不像redis重启后,又是个新生儿。但是它们的互补,又是1+1大于2的那种。
好了,不废话了
一、引入定时任务包 @nestjs/schedule
npm install --save @nestjs/schedule
在 app.module.ts
引入
二、创建定时任务模块和服务
nest g module task
nest g service task
你可以在同一个服务里面声明多个定时任务方法。在 NestJS 中,使用 @nestjs/schedule
库时,你只需要在服务类中为每个定时任务方法添加 @Cron()
装饰器,并指定相应的 cron 表达式。以下是一个示例,展示了如何在同一个服务中声明两个定时任务:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
@Injectable()
export class TasksService {
// 第一个定时任务,每5秒执行一次
@Cron(CronExpression.EVERY_5_SECONDS)
handleEvery5Seconds() {
console.log('Every 5 seconds task executed');
}
// 第二个定时任务,每10秒执行一次
@Cron(CronExpression.EVERY_10_SECONDS)
handleEvery10Seconds() {
console.log('Every 10 seconds task executed');
}
}
三、实现定时任务中同步文章阅读量的任务
更新文章的阅读数据
await this.postService.flushRedisToDB();
// 查询出 key 对应的值,更新到数据库。 做定时任务的时候加上
async flushRedisToDB() {
const keys = await this.redisService.keys(`post_*`);
console.log(keys);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const res = await this.redisService.hashGet(key);
const [, id] = key.split('_');
await this.postRepository.update(
{
id: +id,
},
{
viewCount: +res.viewCount,
},
);
}
}
- 从 Redis 获取键:
const keys = await this.redisService.keys(
post_*);
: 使用 Redis 服务的keys
方法查询所有以post_
开头的键,并将这些键存储在keys
数组中。console.log(keys);
: 打印出所有查询到的键。 - 遍历 Redis 键:
使用
for
循环遍历所有查询到的键。 - 从 Redis 获取哈希值:
const res = await this.redisService.hashGet(key);
: 对于每一个键,使用 Redis 服务的hashGet
方法获取其对应的哈希值,并将结果存储在res
中。 - 解析键以获取 ID:
const [, id] = key.split('_');
: 将键字符串按照_
分割,并取出第二个元素(索引为 1)作为id
。这假设键的格式是post_<id>
。 - 更新数据库:
使用
postRepository.update
方法更新数据库中的记录。{ id: +id, }
: 指定要更新的记录的id
。+id
是将id
字符串转换为数字。{ viewCount: +res.viewCount, }
: 指定要更新的字段及其值。这里将viewCount
字段更新为 Redis 中存储的值,并使用+res.viewCount
将字符串转换为数字。
等到第二天,哈,数据就同步来了
访问:
而产生的后台数据:
抛出问题
如果能看到这里的掘友,若能接下这个问题,说明你已经掌握了吖
问题1:
如何实现一个批量返回redis键值对的方法(这个方法问题2需要用到)
问题2:
用户查询文章列表的时候,如何整理数据后返回文章阅读量呈现给用户查看
来源:juejin.cn/post/7355554711166271540