前言
NestJS关于Redis章节的阐述是在利用Redis进行消息发布订阅,而我们可能需要的是使用Redis来进行数据的记录,所以关于Redis的部分,我是在github上找了一个仓库使用的,liaoliaots/nestjs-redis
在这个过程中,因为我对Redis不熟悉,是在得到了后端同事的协助之后才完成的项目,特此将这些经验与大家分享。
接入Redis
因为我的配置全部来源于Nacos,在之前的文章中记录我的NestJS探究历程(十)------编写插件,我们已经将Nacos封装到了一个单独的npm包中,现在就可以直接使用了,使用Nacos做配置中心的优势就是可以支持配置热更新。
首先是安装npm包:
bash
npm i nacos nestjs-nacos ioredis @liaoliaots/nestjs-redis -S
因为我选择的那个包是基于ioredis进行封装的,所以也需要安装ioredis这个包。
配置Redis连接
ts
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注册成为全局模块的话,需要这样写才不会报错。
ts
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,就会提示找不到依赖的错误 。 好了,现在程序就可以成功的启动起来了。 如果你的项目不动态读取配置中心内容的话会简单的多,可以直接使用环境变量或者编程获取配置即可,各位读者可以根据自己的实际需求决定技术方案。
封装Redis
以下是我们关于Redis的封装,基本上能够满足项目80%的使用。悄悄的告诉大家,这部分代码并不是我写的,😂,哈哈哈,这是我的后端同事写的,在此对他表示感谢。
各位有需要的同学可以把这篇文章收藏下来,将来这部分代码能够直接派上用场。
ts
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:
ts
@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就是利用了链式操作。
ts
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遇到的问题和解决方案了,希望对大家有用😁。