Nest.js进阶系列四:Node.js中使用Redis原来这么简单!

大厂技术 高级前端 Node进阶 前言

大家好,我是 koala,一个有趣且乐于分享的人,这是 Nest.js 实战系列第四篇,我要用真实的场景让你学会使用 Node 主流框架。

回顾一下【Nest入门系列文章】

  • Nest.js 手把手带你实战-项目创建&数据库操作
  • Nest.js 手把手带你实战-实现注册、扫码登陆、jwt认证等
  • Nest.js 手把手带你实战-实现联表查询

前面Nest.js系列的文章中我们其实留了两个可以用redis优化的地方:

  • 一个是我们的在做登录时,通过JWT已经实现了服务端生成token以及验证客户端发送的token信息。
  • 实现对文章点赞功能,采用的是将点赞数据直接写入数据库

JWT token 实现方式, 将基本信息直接放在token中,以便于分布式系统使用, 但是我们没有设置有限期(这个是可以实现的),并且服务端无法主动让token失效。而Redis天然支持过期时间,也能实现让服务端主动使token过期。

当然并不是说JWT token 不如 redis+token实现方案好, 具体看使用的场景,这里我们并不讨论二者孰优孰劣,只是提供一种实现方案,让大家知道如何实现。

1. 认识redis

对于前端的小伙伴来说,Redis可能相对比较陌生,首先认识一下

Redis是什么

Redis是一个开源(BSD许可)的,基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件,是现在最受欢迎的 NoSQL 数据库之一。

其具备如下特性:

  • 速度快
    • 单节点读110000次/s,写81000次/s
    • 基于内存运行,性能高效
    • 用 C 语言实现,离操作系统更近
  • 持久化
    • 数据的更新将异步地保存到硬盘(RDB 和 AOF
  • 多种数据结构
    • 不仅仅支持简单的 key-value 类型数据
    • 还支持:字符串、hash、列表、集合、有序集合
  • 支持多种编程语言等等

Redis 典型使用场景

缓存

缓存可以说是Redis最常用的功能之一了, 合理的缓存不仅可以加快访问的速度,也可以减少后端数据库的压力。

排行系统

利用Redis的列表和有序集合的特点,可以制作排行榜系统,而排行榜系统目前在商城类、新闻类、博客类等等,都是比不可缺的。

计数器应用

计数器的应用基本和排行榜系统一样,都是多数网站的普遍需求,如视频网站的播放计数,电商网站的浏览数等等,但这些数量一般比较庞大,如果存到关系型数据库,对MySQL或者其他关系型数据库的挑战还是很大的,而Redis基本可以说是天然支持计数器应用。

(视频直播)消息弹幕

直播间的在线用户列表,礼物排行榜,弹幕消息等信息,都适合使用Redis中的SortedSet结构进行存储。

例如弹幕消息,可使用ZREVRANGEBYSCORE排序返回,在Redis5.0中,新增了zpopmaxzpopmin命令,更加方便消息处理。

Redis的应用场景远不止这些,Redis对传统磁盘数据库是一个重要的补充,是支持高并发访问的互联网应用必不可少的基础服务之一。

纸上谈兵终觉浅,必须实战一波~

Redis的安装和简单使用,我这里就不一一介绍了,这里贴上我之前写的两篇文章:

  • Redis 安装
  • Redis入门篇-基础使用

可以快速的安装、了解Redis数据类型以及常用的命令。

可视化客户端

在Windows下使用 RedisClient, 在mac下可以使用Redis Desktop Manager

RedisClient下载链接:https://github.com/caoxinyu/RedisClient

下载后直接双击redisclient-win32.x86.2.0.exe文件运行即可

image.png

启动后, 点击server -> add

image.png

连接后就可以看到总体情况了:

image.png

与SQL型数据不同,redis没有提供新建数据库的操作,因为它自带了16(0-15)个数据库(默认使用0库)。在同一个库中,key是唯一存在的、不允许重复的,它就像一把“密钥”,只能打开一把“锁”。键值存储的本质就是使用key来标识value,当想要检索value时,必须使用与value对应的key进行查找.

Redis认识作为文章前置条件,到这里及结束了, 接下来进入正题~

本文主要使用Redis实现缓存功能。

2. 在Nest.js中使用

版本情况:

版本

Nest.js

V8.1.2

项目是基于Nest.js 8.x版本,与Nest.js 9.x版本使用有所不同, 后面的文章专门整理了两个版本使用不同点的说明, 以及如何从V8升级到V9, 这里就不过多讨论。

首先,我们在Nest.js项目中连接Redis, 连接Redis需要的参数:

代码语言:javascript
复制
REDIS_HOST:Redis 域名
REDIS_PORT:Redis 端口号
REDIS_DB:Redis 数据库
REDIS_PASSPORT:Redis 设置的密码

将参数写入.env.env.prod配置文件中:

image.png

使用Nest官方推荐的方法,只需要简单的3个步骤:

  1. 引入依赖文件
代码语言:javascript
复制
npm install cache-manager --save
npm install cache-manager-redis-store --save
npm install @types/cache-manager -D

Nest为各种缓存存储提供统一的API,内置的是内存中的数据存储,但是也可使用 cache-manager来使用其他方案, 比如使用Redis来缓存。

为了启用缓存, 导入ConfigModule, 并调用register()或者registerAsync()传入响应的配置参数。

  1. 创建module文件src/db/redis-cache.module.ts, 实现如下:
代码语言:javascript
复制
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisCacheService } from './redis-cache.service';
import { CacheModule, Module, Global } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';

@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return {
store: redisStore,
host: configService.get('REDIS_HOST'),
port: configService.get('REDIS_PORT'),
db: 0, //目标库,
auth_pass: configService.get('REDIS_PASSPORT') // 密码,没有可以不写
};
},
}),
],
providers: [RedisCacheService],
exports: [RedisCacheService],
})
export class RedisCacheModule {}

  • CacheModuleregisterAsync方法采用 Redis Store 配置进行通信
  • store 属性值redisStore ,表示'cache-manager-redis-store' 库
  • isGlobal 属性设置为true 来将其声明为全局模块,当我们将RedisCacheModuleAppModule中导入时, 其他模块就可以直接使用,不需要再次导入
  • 由于Redis 信息写在配置文件中,所以采用registerAsync()方法来处理异步数据,如果是静态数据, 可以使用register
  1. 新建redis-cache.service.ts文件, 在service实现缓存的读写
代码语言:javascript
复制
import { Injectable, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';

@Injectable()
export class RedisCacheService {
constructor(
@Inject(CACHE_MANAGER)
private cacheManager: Cache,
) {}

cacheSet(key: string, value: string, ttl: number) {
this.cacheManager.set(key, value, { ttl }, (err) => {
if (err) throw err;
});
}

async cacheGet(key: string): Promise<any> {
return this.cacheManager.get(key);
}
}

接下来,在app.module.ts中导入RedisCacheModule即可。

调整 token 签发及验证流程

我们借助redis来实现token过期处理、token自动续期、以及用户唯一登录。

  • 过期处理:把用户信息及token放进redis,并设置过期时间
  • token自动续期:token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间,防止使用时掉线。
  • 户唯一登录:相同的账号,不同电脑登录,先登录的用户会被后登录的挤下线

token 过期处理

在登录时,将jwt生成的token,存入redis,并设置有效期为30分钟。存入redis的key由用户信息组成, value是token值。

代码语言:javascript
复制
// auth.service.ts
async login(user: Partial<User>) {
const token = this.createToken({
id: user.id,
username: user.username,
role: user.role,
});

  • await this.redisCacheService.cacheSet(
  • `${user.id}&amp;${user.username}&amp;${user.role}`,
    
  • token,
    
  • 1800,
    
  • );
    return { token };
    }

在验证token时, 从redis中取token,如果取不到token,可能是token已过期。

代码语言:javascript
复制
// jwt.strategy.ts
  • import { RedisCacheService } from './../core/db/redis-cache.service';
  • export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly authService: AuthService,
    private readonly configService: ConfigService,

    • private readonly redisCacheService: RedisCacheService,
      ) {
      super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: configService.get('SECRET'),

    • passReqToCallback: true,
      

      } as StrategyOptions);
      }

      async validate(req, user: User) {

    • const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);

    • const cacheToken = await this.redisCacheService.cacheGet(

    • `${user.id}&amp;${user.username}&amp;${user.role}`,
      
    • );

    • if (!cacheToken) {

    • throw new UnauthorizedException(&#39;token 已过期&#39;);
      
    • }
      const existUser = await this.authService.getUser(user);
      if (!existUser) {
      throw new UnauthorizedException('token不正确');
      }
      return existUser;
      }
      }

    用户唯一登录

    当用户登录时,每次签发的新的token,会覆盖之前的token, 判断redis中的token与请求传入的token是否相同, 不相同时, 可能是其他地方已登录, 提示token错误。

    代码语言:javascript
    复制
    // jwt.strategy.ts
    async validate(req, user: User) {
    const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
    const cacheToken = await this.redisCacheService.cacheGet(
    ${user.id}&amp;${user.username}&amp;${user.role},
    );
    if (!cacheToken) {
    throw new UnauthorizedException('token 已过期');
    }

  • if (token != cacheToken) {

  • throw new UnauthorizedException(&#39;token不正确&#39;);
    
  • }
    const existUser = await this.authService.getUser(user);
    if (!existUser) {
    throw new UnauthorizedException('token不正确');
    }
    return existUser;
    }

  • token自动续期

    实现方案有多种,可以后台jwt生成access_token(jwt有效期30分钟)和refresh_token, refresh_token有效期比access_token有效期长,客户端缓存此两种token, 当access_token过期时, 客户端再携带refresh_token获取新的access_token。这种方案需要接口调用的开发人员配合。

    我这里主要介绍一下,纯后端实现的token自动续期

    实现流程:

    • ①:jwt生成token时,有效期设置为用不过期
    • ②:redis 缓存token时设置有效期30分钟
    • ③:用户携带token请求时, 如果key存在,且value相同, 则重新设置有效期为30分钟

    设置jwt生成的token, 用不过期, 这部分代码是在auth.module.ts文件中, 不了解的可以看文章 Nest.js 实战系列第二篇-实现注册、扫码登陆、jwt认证

    代码语言:javascript
    复制
    // auth.module.ts
    const jwtModule = JwtModule.registerAsync({
    inject: [ConfigService],
    useFactory: async (configService: ConfigService) => {
    return {
    secret: configService.get('SECRET', 'test123456'),

    • signOptions: { expiresIn: &#39;4h&#39; },  // 取消有效期设置
      
      };
      },
      });

    然后再token认证通过后,重新设置过期时间, 因为使用的cache-manager没有通过直接更新有效期方法,通过重新设置来实现:

    代码语言:javascript
    复制
    // jwt.strategy.ts
    async validate(req, user: User) {
    const token = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
    const cacheToken = await this.redisCacheService.cacheGet(
    ${user.id}&amp;${user.username}&amp;${user.role},
    );
    if (!cacheToken) {
    throw new UnauthorizedException('token 已过期');
    }
    if (token != cacheToken) {
    throw new UnauthorizedException('token不正确');
    }
    const existUser = await this.authService.getUser(user);
    if (!existUser) {
    throw new UnauthorizedException('token不正确');
    }
    • this.redisCacheService.cacheSet(
    • `${user.id}&amp;${user.username}&amp;${user.role}`,
      
    • token,
      
    • 1800,
      
    • );
      return existUser;
      }

    到此,在Nest中实现token过期处理、token自动续期、以及用户唯一登录都完成了, 退出登录时移除token比较简单就不在这里一一上代码了。

    在Nest中除了使用官方推荐的这种方式外, 还可以使用nestjs-redis来实现,如果你存token时, 希望存hash结构,使用cache-manager-redis-store时,会发现没有提供hash值存取放方法(需要花点心思去发现)。

    注意:如果使用nest-redis来实现redis缓存, 在Nest.js 8 版本下会报错, 小伙伴们可以使用@chenjm/nestjs-redis 来代替, 或者参考 issue上的解决方案:Nest 8 + redis bug。

    总结

    源码地址:https://github.com/koala-coding/nest-blog

    Nest.js系列目的:

    1. 希望帮助 Node开发者们熟练掌握 Nest.js 框架,
    2. 帮助想要学习 Node.js 的前端小伙伴们更好的入门一个优秀 Node 框架 该系列会持续更新,感兴趣小伙伴可以star一下,感谢

    回顾一下【Nest入门系列文章】

    • Nest.js 手把手带你实战-项目创建&数据库操作
    • Nest.js 手把手带你实战-实现注册、扫码登陆、jwt认证等
    • Nest.js 手把手带你实战-实现联表查询

    避免文章篇幅太长, 使用redis实现点赞功能放在下一篇中~,欢迎关注