注册

NestJs: 定时任务+redis实现阅读量功能

抛个砖头


不知道大家用了这么久的掘金,有没有对它文章中的阅读量的实现有过好奇呢?


image.png



想象一下,你每次打开一篇文章时,都会有一个数字告诉你多少人已经读过这篇文章。那么这个数字是怎么得出来的呢?



有些人可能会认为,每次有人打开文章,数字就加1,不是很简单吗? 起初我也以为这样,哈哈(和大家站在同一个高度),但(好了,我不说了,继续往下看吧!


引个玉


文章阅读量的统计看似简单,实则蕴含着巧妙的逻辑和高效的技术实现。我们想要得到的阅读量,并非简单的页面刷新次数,而是真正独立阅读过文章的人数。因此,传统的每次页面刷新加1的方法显然不够准确,它忽略了用户的重复访问。


同时,阅读作为一个高频操作,如果每次都直接写入数据库,无疑会给数据库带来巨大的压力,甚至可能影响到整个系统的性能和稳定性。这就需要我们寻找一种既能准确统计阅读量,又能减轻数据库压力的方法。


Redis,这个高性能的内存数据库,为我们提供了解决方案。我们可以利用Redis的键值对存储特性,将用户ID和文章ID的组合作为键,设置一个短暂的过期时间,比如15分钟。当用户首次访问文章时,我们在Redis中为这个键设置一个值,表示该用户已经阅读过这篇文章。如果用户在15分钟内再次访问,我们可以直接判断该键是否存在,如果存在,则不再增加阅读量,否则进行增加。


这种方法的优点在于,它能够准确地统计出真正阅读过文章的人数,而不是简单的页面刷新次数。同时,通过将阅读量先存储在Redis中,我们避免了频繁地写入数据库,从而大大减轻了数据库的压力。


最后,我们还需要考虑如何将Redis中的阅读量最终写入数据库。由于数据库的写入操作相对较重,我们不宜频繁进行。因此,我们可以选择在业务低峰期,比如凌晨2到4点,使用定时任务将Redis中的阅读量批量写入数据库。这样,既保证了阅读量的准确统计,又避免了频繁的数据库写入操作,实现了高效的系统运行。


思路梳理



  1. 😎Redis 助力阅读量统计,方法超好用!✨
  2. 🧐在 Redis 存用户和文章关系,轻松解决多次无效阅读!👏
  3. 💪定时任务来帮忙,Redis 数据写入数据库,不再
    那么接下来就是实现环节

代码层面


项目使用的后端框架为NestJS


配置下redis


一、安装redis plugin


npm install --save redis


二、创建redis模块


image.png


三、初始化连接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文章表添加以下


image.png
这三个字段,在这里我们只拿 阅读量 说事。


访问文章详情接口-阅读量+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;
}
}


  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 引入


image.png


二、创建定时任务模块和服务


nest g module task 
nest g service task

image.png


你可以在同一个服务里面声明多个定时任务方法。在 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,
},
);
}
}


  1. 从 Redis 获取键

    const keys = await this.redisService.keys(post_*);: 使用 Redis 服务的 keys 方法查询所有以 post_ 开头的键,并将这些键存储在 keys 数组中。
    console.log(keys);: 打印出所有查询到的键。


  2. 遍历 Redis 键

    使用 for 循环遍历所有查询到的键。


  3. 从 Redis 获取哈希值

    const res = await this.redisService.hashGet(key);: 对于每一个键,使用 Redis 服务的 hashGet 方法获取其对应的哈希值,并将结果存储在 res 中。


  4. 解析键以获取 ID

    const [, id] = key.split('_');: 将键字符串按照 _ 分割,并取出第二个元素(索引为 1)作为 id。这假设键的格式是 post_<id>


  5. 更新数据库

    使用 postRepository.update 方法更新数据库中的记录。
    { id: +id, }: 指定要更新的记录的 id+id 是将 id 字符串转换为数字。
    { viewCount: +res.viewCount, }: 指定要更新的字段及其值。这里将 viewCount 字段更新为 Redis 中存储的值,并使用 +res.viewCount 将字符串转换为数字。



等到第二天,哈,数据就同步来了


访问:


gh_db79ec2f6f73_860.jpg

而产生的后台数据:


image.png


抛出问题


如果能看到这里的掘友,若能接下这个问题,说明你已经掌握了吖


问题1:


如何实现一个批量返回redis键值对的方法(这个方法问题2需要用到)


问题2:


用户查询文章列表的时候,如何整理数据后返回文章阅读量呈现给用户查看


作者:糖墨夕
来源:juejin.cn/post/7355554711166271540

0 个评论

要回复文章请先登录注册