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

前言

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

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

接入Redis

因为我的配置全部来源于Nacos,在之前的文章中记录我的NestJS探究历程(十)——编写插件,我们已经将Nacos封装到了一个单独的npm包中,现在就可以直接使用了,使用Nacos做配置中心的优势就是可以支持配置热更新。

首先是安装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 {}

这儿,有一个小坑,需要给大家讲一下,NestJS在解析导入的Module的时候,是不能保证顺序的,所以我的NacosConfigModule采用的是全局注册的方式,在之前的文章中记录我的NestJS探究历程(十二)——NestJS启动流程的详细分析,我已经给大家分析过了,NestJS的全局模块是会被视为任意一个模块的依赖模块的。

如果不把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) {
return Number(this.getString(key));
}
/**
* 设置单个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遇到的问题和解决方案了,希望对大家有用😁。

原文链接:https://juejin.cn/post/7321643424951681039 作者:HsuYang

(0)
上一篇 2024年1月9日 下午4:37
下一篇 2024年1月9日 下午4:47

相关推荐

发表回复

登录后才能评论