nest学习4
nest学习(4)
Nest创建微服务
前面的http服务都是单体架构的,所有业务逻辑都在一个服务实现。
项目越来越大后,模块越来越多,可以将业务模块拆成单独的微服务。
微服务之间一般不使用http通信的,因为http会携带大量的header。增大通信开销。一般直接用tcp。
启动一个微服务
使用TCP通信,然后暴露一个端口
@MessagePattern('sum')
sum(numArr: Array<number>): number {
return numArr.reduce((total, item) => total + item, 0);
}
提供方法也不是@Post这些了,而是MessagePattern。
这样我们就创建了一个微服务
再创建一个正常的http服务,然后引用这个微服务。
通过ClientsModule来注入。
然后直接用就行
@Inject('USER_SERVICE')
private userClient: ClientProxy;
@Get('sum')
calc(@Query('num') str) {
const numArr = str.split(',').map((item) => parseInt(item));
return this.userClient.send('sum', numArr);
}
注入的对象就是连接这个微服务的客户端代理。
这样调用sum接口就会调用微服务的sum方法
。
前面在微服务里是用 @MessagePattern 声明的要处理的消息。
如果并不需要返回消息的话,可以用 @EventPattern 声明:
@EventPattern('log')
log(str: string) {
console.log(str);
}
通过抓包我们可以得到他们的通信内容
他们tcp之间通过json消息格式来通信。
Nest的Monorepo和Libarary
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。
如果我们每个服务都用一个git仓库,那微服务多了后,维护成本也变高了,此时可以用monorepo模式。
Nest支持monorepo模式。
nest-cli.json保存着每个项目的基本信息。
每个模块还可以有公共模块,nest支持library。
nest g lib lib1
这样就可以在其他微服务里面直接应用这个lib。
配置中心和注册中心
不同的服务需要使用相同的配置
所以需要一个专门管理配置信息的服务。
注册中心
服务之间会相互依赖,怎么知道对应的服务挂了吗,或者还有哪些节点可用。
微服务在启动的时候,向注册中心注册,销毁的时候也在注册中心注销,并通过定时发送心跳包来回报自己的状态。
在查找其他微服务的时候,去注册中心查一下这个服务的所有节点信息,然后再选一个来用,这个叫做服务发现。
配置中心和注册中心是必备组件
可以做配置中心、注册中心的中间件还是挺多的,比如 nacos、apollo、etcd 等。
etcd 实现注册中心和配置中心。
etcd是一个kv的存储服务,k8s 就是用它来做的注册中心、配置中心。
用法跟redis类似,但可以监听key的变化。
如下
const { Etcd3 } = require('etcd3');
const client = new Etcd3({
hosts: 'http://localhost:2379',
auth: {
username: 'root',
password: 'guang'
}
});
// 保存配置
async function saveConfig(key, value) {
await client.put(key).value(value);
}
// 读取配置
async function getConfig(key) {
return await client.get(key).string();
}
// 删除配置
async function deleteConfig(key) {
await client.delete().key(key);
}
// 服务注册
async function registerService(serviceName, instanceId, metadata) {
const key = `/services/${serviceName}/${instanceId}`;
const lease = client.lease(10);
await lease.put(key).value(JSON.stringify(metadata));
lease.on('lost', async () => {
console.log('租约过期,重新注册...');
await registerService(serviceName, instanceId, metadata);
});
}
// 服务发现
async function discoverService(serviceName) {
const instances = await client.getAll().prefix(`/services/${serviceName}`).strings();
return Object.entries(instances).map(([key, value]) => JSON.parse(value));
}
// 监听服务变更
async function watchService(serviceName, callback) {
const watcher = await client.watch().prefix(`/services/${serviceName}`).create();
watcher.on('put', async event => {
console.log('新的服务节点添加:', event.key.toString());
callback(await discoverService(serviceName));
}).on('delete', async event => {
console.log('服务节点删除:', event.key.toString());
callback(await discoverService(serviceName));
});
}
// (async function main() {
// await saveConfig('config-key', 'config-value');
// const configValue = await getConfig('config-key');
// console.log('Config value:', configValue);
// })();
(async function main() {
const serviceName = 'my_service';
await registerService(serviceName, 'instance_1', { host: 'localhost', port:3000 });
await registerService(serviceName, 'instance_2', { host: 'localhost', port:3002 });
const instances = await discoverService(serviceName);
console.log('所有服务节点:', instances);
watchService(serviceName, updatedInstances => {
console.log('服务节点有变动:', updatedInstances);
});
})();
- 不同服务的配置需要统一管理,并且在更新后通知所有的服务,所以需要配置中心。
- 微服务的节点可能动态的增加或者删除,依赖他的服务在调用之前需要知道有哪些实例可用,所以需要注册中心。
- 服务启动的时候注册到注册中心,并定时续租期,调用别的服务的时候,可以查一下有哪些服务实例可用,也就是服务注册、服务发现功能。
集成到Nest
- 先写成一个provider
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Etcd3 } from 'etcd3';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'ETCD_CLIENT',
useFactory() {
const client = new Etcd3({
hosts: 'http://localhost:2379',
auth: {
username: 'root',
password: 'guang'
}
});
return client;
}
}
],
})
export class AppModule {}
这样就可以直接注入该provider。
封装动态模块
import { DynamicModule, Module, ModuleMetadata, Type } from '@nestjs/common';
import { EtcdService } from './etcd.service';
import { Etcd3, IOptions } from 'etcd3';
export const ETCD_CLIENT_TOKEN = 'ETCD_CLIENT';
export const ETCD_CLIENT_OPTIONS_TOKEN = 'ETCD_CLIENT_OPTIONS';
@Module({})
export class EtcdModule {
static forRoot(options?: IOptions): DynamicModule {
return {
module: EtcdModule,
providers: [
EtcdService,
{
provide: ETCD_CLIENT_TOKEN,
useFactory(options: IOptions) {
const client = new Etcd3(options);
return client;
},
inject: [ETCD_CLIENT_OPTIONS_TOKEN]
},
{
provide: ETCD_CLIENT_OPTIONS_TOKEN,
useValue: options
}
],
exports: [
EtcdService
]
};
}
}
通过动态模块,将options通过forRoot动态传入。然后编写etcdSerivce,其他地方用的时候直接注入该动态模块调用EtcdService的方法就行。
基于gRPC实现跨语言的微服务通信
多语言实现的微服务之间如何通信呢,http的话是文本传输,效率低。跨语言调用服务一般会用gRPC。
将server改造成grpc的微服务
import { NestFactory } from '@nestjs/core';
import { GrpcOptions, Transport } from '@nestjs/microservices';
import { GrpcServerModule } from './grpc-server.module';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.createMicroservice<GrpcOptions>(GrpcServerModule, {
transport: Transport.GRPC,
options: {
url: 'localhost:8888',
package: 'book',
protoPath: join(__dirname, 'book/book.proto'),
},
});
await app.listen();
}
bootstrap();
tpc改为GRPC。
在指定位置场景book.proto文件。
src/book/book.proto
// 版本语法
syntax = "proto3";
// 包名称
package book;
// 提供的服务方法
service BookService {
rpc FindBook (BookById) returns (Book) {}
}
// 定义参数BookById的格式
message BookById {
int32 id = 1;
}
// 定义返回的Book的格式
message Book {
int32 id = 1;
string name = 2;
string desc = 3;
}
这是protocol buffer 的语法,因为要跨语言通信,不同语言语法不同,所以需要一个通用的通信语言。
这里book.proto只是定义了格式,具体实现需要在controller中实现。
@GrpcMethod('BookService', 'FindBook')
findBook(data: { id: number}) {
const items = [
{ id: 1, name: '前端调试通关秘籍', desc: '网页和 node 调试' },
{ id: 2, name: 'Nest 通关秘籍', desc: 'Nest 和各种后端中间件' },
];
return items.find(({ id }) => id === data.id);
}
通过@GrpcMethod标识为grpcde的远程盗用方法。
并在nest-cli.json中添加assets配置,build的时候可以复制到disst目录下。
然后在另一个服务可以联该grpc服务了。
先在module中import进来。
同样调用方也是需要book.proto文件的,不然不知道怎么解析协议数据。文件内容跟grpc服务保持一致即可。
然后就可以在controller里面去掉用了
通过 @Inject注入,在模块初始化的时候,拿到BookService实例。然后就可以调用他的方法了。
这就是基于grpc的远程方法调用。
通过 protocol buffer 的语法定义通信数据的格式,比如 package、service 等。
然后再server端实现方法,在client端调用该方法。
java里面是,安装这两个依赖
定义同样的proto文件。
然后创建对应的service即可。
在client端调用该java服务的时候,跟调用nest服务是一样的。
小结
- 不同语言服务可以用grpc来实现互相调用,而不是http。
- 实现方式是protocol buffer语法来定义通信数据格式,定义package和service。
- 然后在server端实现方法,client通过注入拿到实例,直接调用。
Prisma
Typeorm是一个传统的ORM框架,表映射到entity类,把表的关联映射成enttiy类的属性关联。
完成entity和表的映射之后,只需要调用userRepositor和postRepository的find,delete,save等api,typeorm会自动生成对应的sql语句并执行。
这就是
ORM (Object Relational Mapping)
,对象和关系型数据库映射。
而Prisma不是这样的。他没有entity类的存在。
用法
定义model,
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient();
async function test1() {
await prisma.user.create({
data: {
name: 'test',
email: '111@tesst.com'
}
});
await prisma.user.create({
data: {
name: 'test',
email: '222@test.com'
}
});
const users = await prisma.user.findMany();
console.log(users);
}
test1();
创建 PrismaClient,用 create 方法创建了 2 个 user,然后查询出来
其他命令
- init:创建 schema 文件
- generate: 根据 shcema 文件生成 client 代码
- db:同步数据库和 schema
- migrate:生成数据表结构更新的 sql 文件
- studio:用于 CRUD 的图形化界面
- validate:检查 schema 文件的语法错误
- format:格式化 schema 文件
- version:版本信息
model 部分定义和数据库表的对应关系:
@id 定义主键
@default 定义默认值
@map 定义字段在数据库中的名字
@db.xx 定义对应的具体类型
@updatedAt 定义更新时间的列
@unique 添加唯一约束
@relation 定义外键引用
@@map 定义表在数据库中的名字
@@index 定义索引
@@id 定义联合主键
model Department {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
createTime DateTime @default(now())
updateTime DateTime @updatedAt
employees Employee[]
}
model Employee {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
phone String @db.VarChar(30)
deaprtmentId Int
department Department @relation(fields: [deaprtmentId], references: [id])
}
创建时间我们使用 @default(now()) 的方式指定,这样插入数据的时候会自动填入当前时间。
更新时间使用 @updatedAt,会自动设置当前时间。
员工和部门是多对一关系,在员工那一侧添加一个 departmentId 的列,然后通过 @relation 声明 deaprtmentId 的列引用 department 的 id 列。
CRUD api
create、crateMany、update、updateMany、delete、deleteMany、findMany、findFirst、findFirstOrThrow、findUnique、findUniqueOrThrow。
统计相关: count、aggregate、groupBy
// 返回的最大值、最小值、计数、平均值
async function test12() {
const res = await prisma.aaa.aggregate({
where: {
email: {
contains: 'xx.com'
}
},
_count: {
_all: true,
},
_max: {
age: true
},
_min: {
age: true
},
_avg: {
age: true
}
});
console.log(res);
}
// 按照 email 分组,过滤出平均年龄大于 2 的分组,计算年龄总和返回。
async function test13() {
const res = await prisma.aaa.groupBy({
by: ['email'],
_count: {
_all: true
},
_sum: {
age: true,
},
having: {
age: {
_avg: {
gt: 2,
}
},
},
})
console.log(res);
}
Nest集成prisma
先primsa init生成prisma文件
创建对应model
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Department {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
createTime DateTime @default(now())
updateTime DateTime @updatedAt
employees Employee[]
}
model Employee {
id Int @id @default(autoincrement())
name String @db.VarChar(20)
phone String @db.VarChar(30)
deaprtmentId Int
department Department @relation(fields: [deaprtmentId], references: [id])
}
npx prisma migrate reset
npx prisma migrate dev --name init
rest后直接初始化,这样数据库就会有两张表。
创建一个PrimsaService,方便调用
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor() {
super({
log: [
{
emit: 'stdout',
level: 'query'
}
]
})
}
async onModuleInit() {
await this.$connect();
}
}
使用直接注入该Service即可
import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Prisma } from '@prisma/client';
@Injectable()
export class DepartmentService {
@Inject(PrismaService)
private prisma: PrismaService;
async create(data: Prisma.DepartmentCreateInput) {
return await this.prisma.department.create({
data,
select: {
id: true
}
});
}
}
这样就可以在nest里面使用Prisma的CRUD了。