nestjs 接入 redis

790 阅读7分钟

前言

有时候我们的数据库压力很大,为了减少数据库压力,除了分表、分库等操作,还有一种比较常见的操作就是引用redis

redis 和我们的关系型数据库有点不一样,我们的重要数据一般仍然要存放到数据库,一般访问量大,并且对于实时性、安全性要求不是那么严格(即数据延迟,丢失都不会存在实际隐患,最多体验稍微差点,但对于整体提升效果来说是值得的)

特点:redis 数据主要以内存为主,内存的高效率,这也奠定了其性能优势,并且庞大并发下,使用多组 redis 等方式提升吞吐量;且这不代表其不会储存到本地,当一定次数、时间后,会自动更新本地缓存(更新更新过于频繁会丢失一些性能优势),因此,如果需要 redis 保存到本地的效果,需要最后部分数据更新丢失的接受能力(也许可以调整后自动保存本地)

定位: redis 大多数被是用来分担数据库压力的,毕竟服务器可以不断增加,相当于没有瓶颈,而数据库并不是,即使有分库等优化操作,终归是远不能和服务器比的,正因为如此 redis 也被叫数据库

使用情况:除了特殊场景,我们也基本上不会将 redis 直接给前端用户访问,而是也经过一或多台服务器连接访问 redis,多台服务器访问多台redis都是有可能的,因此,一台 redis 可能对接多台服务器,一台服务器也可能连接多个 redis(可能根据不同功能模块连接不同 redis)

ps:如果看到什么功能就一味依靠 redis,有时不仅仅会增加自己和运维的工作,甚至还会降低性能,为什么这么说,例如:在一个小项目中,只有一台服务器情况,自己语言库里面的哈希表性能就一定比 redis 效率低么,那可不一定哈,很不仅不会,由于少了一步交互,不仅性能高,还节省硬件成本,用一个东西一定是看情况需要才去用

安装 redis 到主机

redis

安装 redis

brew install redis

前台启动redis,control + c 取消运行,也就意味着关闭终端停止运行

redis-server

使 redis 在后端也能够运行,启动命令,如果没安装服务,会先安装服务

brew services start redis

查看 redis 服务的运行信息

brew services info redis

停止 redis 的运行,如果不主动停止将会一直运行

brew services stop redis

可以通过 vscodedatabase client 连接 redis,输入 ip 端口号就可以连接看情况了(设置了密码后就输入密码就行了),就可以看到 redis 数据库的情况了

image.png

应用到项目

我们在后端连接 redis 需要用到相关库,因此需要导入 redis 库,javascript的redis文档

yarn add redis

(也可以导入 ioredis,ioredis看着也更好用一些,不过最后一个案例有,就不多介绍了哈)
yarn add ioredis

配置我们的 redis 服务文件,这里只是简单介绍

一般使用 set、get 等就可以了,如果真的需要一个组合大对象的话,可以考虑hSet、gGet

ps:下面几种 redis 的 provider 和三方的使用,如果有不明白的,我们的 module模块 里面有介绍的(第一版内容很少,后面改动增加了不少内容哈)

创建 service 使用

里面的一些函数主要作为使用案例介绍,实际可以直接使用 service.client 即可,多个模块导入,可以将 client 搞成单例,下面主要是用来讲解方法了,实际不一定这么使用哈

import { envConfig } from 'src/app.config';
import { createClient, RedisClientType } from '@redis/client';
import { Injectable } from '@nestjs/common';

//虽然这么写,但是实际不这么用
@Injectable()
export class RedisService {
    client: RedisClientType; //开放的,外面也可以通过这个参数直接使用
    constructor() {
        this.client = createClient({
            socket: {
                host: envConfig.REDIS_HOST,
                port: Number(envConfig.REDIS_PORT),
            },
            // url: `redis://alice:foobared@awesome.redis.server:6380`,
            // username: '',
            // password: '',
            //默认就是这个,一般一个项目就用一个,可能多个项目用一台redis服务器,看情况来就行
            // database: 0, 
        })
        this.client.connect()
    }
    
    //下面的以介绍案例为主,实际上推荐直接使用 client
    //下面的操作可以封装使用,但肯定比直接使用上面的效率低一些,自己看情况
    //根据 hash 设置 value值
    async set(key: string, val: string) {
        await this.client.set(key, val);
    }
    
    //根据 hash 获取内容
    async get(key: string) {
        return await this.client.get(key);
    }
    
    //根据某个hash 删除
    async delete(key: string) {
        await this.client.del(key);
    }
    
    //根据 hash 设置 一个大对象的某个 key 和 值,key 和 value
    async hSet(hash: string, key: string, val: string) {
        await this.client.hSet(hash, key, val);
    }
    
    //根据 hash 获取对应的大对象 或 某个key对应的值
    async hGet(hash: string, key?: string) {
        if (key) {
            return await this.client.hGet(hash, key);
        }
        return await this.client.hGetAll(hash)
    }

    //获取某个大对象的所有values
    async hGetVals(hash: string) {
        return await this.client.hVals(hash);
    }
    
    //删除hash对应的对象某个key、对象
    async hDelete(hash: string, key?: string) {
        if (key) {
            return await this.client.hDel(hash, key);
        }
        return await this.client.del(hash);
    }
}

上面的 service 使用起来很方便,符合习惯,哪个模块用到了,直接导入即可,但是似乎也没那么好用,service感觉很多余哈

useFactory版本(provide + inject)(改进后推荐)

也是要导入 redis,然后在指定模块的 module 的 providers 中添加下面操作即可直接使用

//添加标识,都写到一个文件,封装一个 InjectMyRedis 减少代码
export const redis_provide_identifier = 'REDIS_CLIENT'
export const InjectMyRedis = () => Inject(redis_provide_identifier);

providers: [
    {
        provide: 'redis_provide_identifier',
        async useFactory() {
            const client = createClient({
                socket: {
                    host: envConfig.REDIS_HOST,
                    port: Number(envConfig.REDIS_PORT),
                },
            });
            await client.connect();
            return client;
        },
    },
],

在本模块其他文件中,直接使用 @Inject 引入 redis,即可直接使用

export class XXXController {
    constructor(
        @Inject('redis_provide_identifier') private readonly redisClient: RedisClientType,
    ) {}

嫌弃使用标识麻烦,或者报错,可以声明换一个类型,或者创建一个新的装饰器,创建一个装饰器文件(例如:redis.decorator.ts),写到里面即可

import { Inject } from "@nestjs/common";

export const redis_provide_identifier = 'REDIS_CLIENT'
export const InjectMyRedis = () => Inject(redis_provide_identifier);

ps:这个写的代码也没比第一种少多少,只有一个模块使用还行,多个模块,看看怎么引入吧,改进一下挺推荐的

改进方案(前面模块讲了全局,自己再按照上面改进装饰器,基本上和后面的三方差不多了)

//redis.decorator.ts
import { Inject } from "@nestjs/common";

export const redis_provide_identifier = 'REDIS_CLIENT'
export const InjectMyRedis = () => Inject(redis_provide_identifier);

//redis.module.ts
type ConfigType = {
    ...
}

@Global()
@Module({})
export class RedisModule {
    //这种配置我们一般在不同模块配置,然后就可以直接在导入的模块使用了
    static register(config: ConfigType): DynamicModule {
        return {
            module: RedisModule,
            providers: [
                {
                    provide: redis_provide_identifier,
                    async useFactory() {
                        const client = createClient({
                            socket: {
                                host: envConfig.REDIS_HOST,
                                port: Number(envConfig.REDIS_PORT),
                            },
                        });
                        await client.connect();
                        return client;
                    },
                }
            ],
            exports: [ConfigService],
        };
    }
}

使用案例

//app.module.ts中动态注入即可
imports: [
    RedisModule.register({})
]

//使用,其他模块不用导入模块直接使用
export class XXXController {
    constructor(
        @InjectMyRedis private readonly redisClient: RedisClientType,
    ) {}

使用三方组件库(@liaoliaots/nestjs-redis、ioredis)-推荐

这个除了使用方便,还多了一些文档等功能,可以直接入手使用,嫌库多没必要,可以用用第二种改进方案

另外这里发现还引入了 ioredis,个人也比较推荐这个哈,可以使用这个库 ioredis,看着感觉它比第一个案例的 redis 库看着优秀点,实际可能也差不多哈

yarn add @liaoliaots/nestjs-redis ioredis

然后和数据库一样,直接在 app.module 动态导入 redis module,想知道别人怎么封装的模块导入,可以看源码,实际上官网的模块的案例代码应该足够理解了

imports: [
    RedisModule.forRoot({
        //closeClient: true, //redis挂了,nestjs也挂掉
        //readyLog: true, 在客户端展示日志
        config: {
            host: envConfig.REDIS_HOST,
            port: Number(envConfig.REDIS_PORT),
            db: Number(envConfig.REDIS_DB),
        },
    }),
]

上面是全局导入,之后直接注入方式获取我们的 redis 对象即可,另外可以看到 ioredis 确实比 redis 的文档多一些,用着舒服一些哈,也更加人性化

import { Redis } from 'ioredis';

@InjectRedis() private readonly redis: Redis,

this.redis.set
this.redis.get
this.redis.hset
this.redis.hget
this.redis.hgetBuffer
//假如需要过期,过期后会自动删除,例如:某些公告、token
this.redis.set("key", "data", "EX", 60); //假如需要设置过期
......

最后

redis 使用就是如此简单,实际上其还有不少功能,自己也不乏尝试一下,看了别人的模块化搞得有点舒服的,也可以自己参考文档尝试,并不复杂,不管是多路还是单路,自己看着用即可