likes
comments
collection
share

记录我的NestJS探究历程(十四)——接入Redis

作者站长头像
站长
· 阅读数 7

前言

NestJS关于Redis章节的阐述是在利用Redis进行消息发布订阅,而我们可能需要的是使用Redis来进行数据的记录,所以关于Redis的部分,我是在github上找了一个仓库使用的,liaoliaots/nestjs-redis

在这个过程中,因为我对Redis不熟悉,是在得到了后端同事的协助之后才完成的项目,特此将这些经验与大家分享。

接入Redis

首先是安装npm包:

npm i nacos nestjs-nacos ioredis @liaoliaots/nestjs-redis -S

因为我选择的那个包是基于ioredis进行封装的,所以也需要安装ioredis这个包。

配置Redis连接

import { ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envFiles,
    }),
    NacosConfigModule.register(
      {
        url: process.env.NACOS_ADDRESS,
        // 私密项目配置,展示是假的
        namespace: 'xxx',
        timeout: 30000,
      },
      true,
    ),
    // 读取nacos注册redis服务
    RedisModule.forRootAsync({
      useFactory: async (nacosService: NacosConfigService) => {
        const config = await nacosService.getKeyItemConfig({
          dataId: APP_KEY,
        });
        const result = JSON.parse(config);
        return {
          config: {
            host: result.REDIS.HOST,
            port: result.REDIS.PORT,
            db: result.REDIS.DB,
            password: result.REDIS.PASSWORD,
            keyPrefix: result.REDIS.PREFIX,
            onClientCreated: () => {
              logger.log('redis client has created');
            },
          },
        };
      },
      inject: [NacosConfigService],
    }),
  ],
})
export class AppModule {}

如果不把nacos注册成为全局模块的话,需要这样写才不会报错。

import { ConfigModule } from '@nestjs/config';
import { Module } from '@nestjs/common';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: envFiles,
    }),
    // 读取nacos注册redis服务
    RedisModule.forRootAsync({
      imports:[
        NacosConfigModule.register(
          {
            url: process.env.NACOS_ADDRESS,
            // 私密项目配置,展示是假的
            namespace: 'xxx',
            timeout: 30000,
          }
        )
      ],
      useFactory: async (nacosService: NacosConfigService) => {
        const config = await nacosService.getKeyItemConfig({
          dataId: APP_KEY,
        });
        const result = JSON.parse(config);
        return {
          config: {
            host: result.REDIS.HOST,
            port: result.REDIS.PORT,
            db: result.REDIS.DB,
            password: result.REDIS.PASSWORD,
            keyPrefix: result.REDIS.PREFIX,
            onClientCreated: () => {
              logger.log('redis client has created');
            },
          },
        };
      },
      inject: [NacosConfigService],
    }),
  ],
})
export class AppModule {}

这个报错的原因是因为在解析到RedisModule的时候,NacosConfigModule不一定已经注册好了,因为RedisModule依赖NacosConfigService这个Provider,此时还找不到NacosConfigService,就会提示找不到依赖的错误。 好了,现在程序就可以成功的启动起来了。 记录我的NestJS探究历程(十四)——接入Redis 如果你的项目不动态读取配置中心内容的话会简单的多,可以直接使用环境变量或者编程获取配置即可,各位读者可以根据自己的实际需求决定技术方案。

封装Redis

以下是我们关于Redis的封装,基本上能够满足项目80%的使用。悄悄的告诉大家,这部分代码并不是我写的,😂,哈哈哈,这是我的后端同事写的,在此对他表示感谢。

各位有需要的同学可以把这篇文章收藏下来,将来这部分代码能够直接派上用场。

import { RedisService } from '@liaoliaots/nestjs-redis';
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';

@Injectable()
export class RedisRepository {
  private redisClient: Redis;

  constructor(private readonly redisService: RedisService) {
    this.redisClient = this.redisService.getClient();
  }

  /**
   * 加锁
   * @param key
   * @param ttl
   */
  public async lock(key: string, ttl = 1) {
    key += ':lock';
    const res = await this.redisClient.set(key, 'Y', 'EX', ttl, 'NX');
    return res === 'OK';
  }

  /**
   * 释放锁
   * @param key
   */
  public unlock(key: string) {
    key += ':lock';
    this.redisClient.del(key);
  }

  /**
   * 删除单个key
   * @param key
   */
  public del(key: string) {
    this.redisClient.del(key);
  }

  /**
   * 获取单个number数据
   * @param key
   */
  public async getNumber(key: string) {
    const val = await this.getString(key);
    return Number(val);
  }

  /**
   * 设置单个number数据
   * @param key
   * @param value
   * @param ttl
   */
  public setNumber(key: string, value: number, ttl: number) {
    this.redisClient.setex(key, ttl, value);
  }

  public async getString(key: string) {
    return this.redisClient.get(key);
  }

  public setString(key: string, value: string, ttl: number) {
    this.redisClient.setex(key, ttl, value);
  }

  /**
   * 设置hash的field
   * @param key
   * @param field
   * @param value
   * @param ttl
   */
  public setAttr(
    key: string,
    field: string,
    value: string | number,
    ttl: number | string,
  ) {
    this.redisClient
      .multi({ pipeline: true })
      .hset(key, field, value)
      .expire(key, ttl);
  }

  /**
   * 移除hash的field
   * @param key
   * @param field
   * @returns
   */
  public removeAttr(key: string, field: string) {
    return this.redisClient.hdel(key, field);
  }

  /**
   * 获取hash的field
   * @param key
   * @param field
   * @returns
   */
  public getAttr(key: string, field: string) {
    return this.redisClient.hget(key, field);
  }

  /**
   * 给hash属性增加整数值
   * @param key
   * @param field
   * @param value
   * @param ttl
   */
  public async incrAttr(
    key: string,
    field: string,
    value: string | number,
    ttl: number | string,
  ) {
    const res = await this.redisClient.hincrby(key, field, value);
    this.redisClient.expire(key, ttl);
    return res;
  }

  /**
   * 给hash属性增加小数值
   * @param key
   * @param field
   * @param value
   * @param ttl
   */
  public async incrFloatAttr(
    key: string,
    field: string,
    value: string | number,
    ttl: number | string,
  ) {
    const res = await this.redisClient.hincrbyfloat(key, field, value);
    this.redisClient.expire(key, ttl);
    return res;
  }

  /**
   * 获取hash多个属性
   * @param key
   * @param fields
   */
  public async getAttrs(key: string, fields: string[] = null) {
    const res = await this.redisClient.hmget(key, ...fields);
    return new Map(fields.map((item, i) => [item, res[i]]));
  }

  /**
   * 获取hash所有
   * @param key
   */
  public async getAllAttrs(key: string) {
    return this.redisClient.hgetall(key);
  }

  /**
   * 获取列表
   * @param key
   * @param start
   * @param end
   */
  public async getList(key: string, start: number, end: number) {
    return this.redisClient.lrange(key, start, end);
  }

  /**
   * 获取Set集合中的所有内容
   * @param key
   */
  public getSetItems(key: string) {
    return this.redisClient.smembers(key);
  }

  public async hasSetItem(key: string, item: string) {
    const result = await this.redisClient.sismember(key, item);
    return result === 1;
  }

  /**
   * 向Set集合中增加一个堆值
   * @param key
   * @param items
   * @param ttl
   * @returns
   */
  public async addSetItems(key: string, items: string[], ttl: number) {
    return new Promise((resolve, reject) => {
      this.redisClient
        .multi()
        .sadd(key, items)
        .expire(key, ttl)
        .exec((err, result) => {
          if (err) {
            reject(err);
          } else {
            const flag = result.every((v) => v[1] === 1);
            resolve(flag ? 'ok' : 'failed');
          }
        });
    });
  }

  /**
   * 添加列表项并trim长度
   * @param key
   * @param items
   * @param ttl
   * @param size
   * @param limitNum
   */
  public addItems(
    key: string,
    items: string[],
    ttl: number,
    size = 20,
    limitNum = 500,
  ) {
    return this.redisClient
      .multi()
      .lpush(key, ...items)
      .llen(key)
      .expire(key, ttl)
      .exec((err, result) => {
        const len = result[1][1] as number;
        if (len > limitNum) {
          this.redisClient.ltrim(key, 0, size - 1);
        }
      });
  }

  /**
   * 判断某个值是否已经存在于列表中
   * @param key
   * @param item
   * @returns
   */
  public async hasItem(key: string, item: string) {
    const result = await this.redisClient.lrange(key, 0, -1);
    return result.includes(item);
  }

  /**
   * 添加单个列表项并trim长度
   * @param key
   * @param item
   * @param ttl
   * @param size
   * @param limitNum
   */
  public addItem(
    key: string,
    item: string,
    ttl: number,
    size = 20,
    limitNum = 500,
  ) {
    return this.addItems(key, [item], ttl, size, limitNum);
  }
}

使用Redis记录数据

在Redis的操作过程中,给大家分享几个我同事传递的关键点。

在Redis的操作中一定要使用它的原子操作API,就比如一个场景的业务需求,我需要记录用户的抽奖次数,切记不要先从Redis读取用户之前的抽奖次数然后在程序中加值,再调用Redis的赋值API,这儿的问题就是,在并发的场景下是绝对要出问题的,因为可能在很短的时间内,别人也在访问,就有可能导致新值覆盖旧的值。

以下是一个Good Case:

@Injectable()
export class SomeBusinessService {
  // 在业务中注入我们之前封装的那个RedisRepository
  constructor(
    protected readonly redisRepo: RedisRepository,
  ) {}

  /**
   * 抽奖,并记录抽奖所得的礼物 (业务代码,已做脱敏处理)
   */
  public requestLottery() {
    const size = 10
    const storageKey = 'some-key';
    // 使用的是Redis的hincrby而不是先通过代码获取再设置。
    this.redisRepo.incrAttr(
      storageKey,
      userId,
      size,
      // 获取到30天的毫秒数,来源于我们的项目代码
      // this.utilService.getSeconds('day', 30),
      30000000
    );
    return result;
  }
}

然后是关于Redis的Key的规则,根据我同事告诉我他这么多年的开发经验,一般的规则是${appName}:${moduleName}:${businessName},这样的规则好维护,容易区分,但是也不要把Key搞的特别长,否则会降低查询的性能,这个大家可以根据自己的项目酌情进行处理。

最后是可以利用Redis的链式操作提高性能,在之前的代码中有一个addItems就是利用了链式操作。

class RedisRepository {
    
  public addItems(
    key: string,
    items: string[],
    ttl: number,
    size = 20,
    limitNum = 500,
  ) {
    // 链式操作
    return this.redisClient
      .multi()
      .lpush(key, ...items)
      .llen(key)
      .expire(key, ttl)
      .exec((err, result) => {
        const len = result[1][1] as number;
        if (len > limitNum) {
          this.redisClient.ltrim(key, 0, size - 1);
        }
      });
  }
}

如果各位对Redis感兴趣的话,更多详细的学习资料还需要查看官方的文档才行。

以上就是我在BFF项目关于在接入Redis遇到的问题和解决方案了,希望对大家有用😁。