目录

nest学习4

nest学习(4)

Nest创建微服务

前面的http服务都是单体架构的,所有业务逻辑都在一个服务实现。

https://i-blog.csdnimg.cn/direct/4036aeb278bb4d6f90eb574dfc8ed1ea.png

项目越来越大后,模块越来越多,可以将业务模块拆成单独的微服务。

https://i-blog.csdnimg.cn/direct/9cfd9c40fd724978883df5452b3cdc1b.png

微服务之间一般不使用http通信的,因为http会携带大量的header。增大通信开销。一般直接用tcp。

启动一个微服务

https://i-blog.csdnimg.cn/direct/c3b63db90ac449d09b3a6d67f482b397.png

使用TCP通信,然后暴露一个端口

@MessagePattern('sum')
sum(numArr: Array<number>): number {
    return numArr.reduce((total, item) => total + item, 0);
}

提供方法也不是@Post这些了,而是MessagePattern。

这样我们就创建了一个微服务

https://i-blog.csdnimg.cn/direct/314dd86848b64c4ea718dd2f7d9ac06d.png

再创建一个正常的http服务,然后引用这个微服务。

通过ClientsModule来注入。

https://i-blog.csdnimg.cn/direct/6ac97d69cb784b9e96953062423c790f.png

然后直接用就行

@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);
}

通过抓包我们可以得到他们的通信内容

https://i-blog.csdnimg.cn/direct/a7fe739db8d24ce08274c31e29673247.png

他们tcp之间通过json消息格式来通信。

Nest的Monorepo和Libarary

Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Microsoft 等。

如果我们每个服务都用一个git仓库,那微服务多了后,维护成本也变高了,此时可以用monorepo模式。

https://i-blog.csdnimg.cn/direct/a5cb0c962f8f434fab55822882432bef.png

Nest支持monorepo模式。

https://i-blog.csdnimg.cn/direct/b84c59c6c0ba4bca8db268006a733a20.png

https://i-blog.csdnimg.cn/direct/d03db09d86c4435ab6fe601aca07e6bd.png

nest-cli.json保存着每个项目的基本信息。

每个模块还可以有公共模块,nest支持library。

nest g lib lib1

https://i-blog.csdnimg.cn/direct/27000e853948488c8239b6b19509cc12.png

https://i-blog.csdnimg.cn/direct/437cb3eacc9145d9b230ab8af1497f15.png

https://i-blog.csdnimg.cn/direct/592ba3f7c2cf4dde8f9b5a3ec1908246.png

https://i-blog.csdnimg.cn/direct/9a266fe3deb244babb37837b896393a1.png

这样就可以在其他微服务里面直接应用这个lib。

https://i-blog.csdnimg.cn/direct/22a45e0137a94c679441453ed10ae15a.png

https://i-blog.csdnimg.cn/direct/a5d0fe1e95914e8db646ff2334994b10.png

配置中心和注册中心

不同的服务需要使用相同的配置

https://i-blog.csdnimg.cn/direct/6bd14b825b464bcbbc173d6f1c5646c9.png

所以需要一个专门管理配置信息的服务。

注册中心

服务之间会相互依赖,怎么知道对应的服务挂了吗,或者还有哪些节点可用。

https://i-blog.csdnimg.cn/direct/93701969085e434facd7ca9f1eec3408.png

微服务在启动的时候,向注册中心注册,销毁的时候也在注册中心注销,并通过定时发送心跳包来回报自己的状态。

在查找其他微服务的时候,去注册中心查一下这个服务的所有节点信息,然后再选一个来用,这个叫做服务发现。

https://i-blog.csdnimg.cn/direct/09e86110d14040c6b992576c67ccab4f.png

https://i-blog.csdnimg.cn/direct/f486311aab51409aab66d83f9b83bdfa.png

配置中心和注册中心是必备组件

可以做配置中心、注册中心的中间件还是挺多的,比如 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的方法就行。

https://i-blog.csdnimg.cn/direct/f8aabaebd7d54e1fa619035ccb21811b.png

基于gRPC实现跨语言的微服务通信

多语言实现的微服务之间如何通信呢,http的话是文本传输,效率低。跨语言调用服务一般会用gRPC。

https://i-blog.csdnimg.cn/direct/4a14d455dc644d7b940068e5ed580250.png

将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目录下。

https://i-blog.csdnimg.cn/direct/08d0c3478dba43c2ad19fab974d54e6f.png

然后在另一个服务可以联该grpc服务了。

https://i-blog.csdnimg.cn/direct/21ac997a167041aea532d3ff12acc8e8.png

先在module中import进来。

同样调用方也是需要book.proto文件的,不然不知道怎么解析协议数据。文件内容跟grpc服务保持一致即可。

然后就可以在controller里面去掉用了

https://i-blog.csdnimg.cn/direct/c67d9c6a48414e2bbf0d1c31899f02dd.png

通过 @Inject注入,在模块初始化的时候,拿到BookService实例。然后就可以调用他的方法了。

https://i-blog.csdnimg.cn/direct/e5466425b11e40b7a9271df1404da2b4.png

这就是基于grpc的远程方法调用。

通过 protocol buffer 的语法定义通信数据的格式,比如 package、service 等。

然后再server端实现方法,在client端调用该方法。

java里面是,安装这两个依赖

https://i-blog.csdnimg.cn/direct/1d3d28fe451a46ababbd1feccc1d86b3.png

定义同样的proto文件。

https://i-blog.csdnimg.cn/direct/9c7ccfc80a2e4cb4b35f1d1e6ac29ad7.png

然后创建对应的service即可。

https://i-blog.csdnimg.cn/direct/a04662e622b848eaaa66145b0c2ba8b8.png

在client端调用该java服务的时候,跟调用nest服务是一样的。

小结
  • 不同语言服务可以用grpc来实现互相调用,而不是http。
  • 实现方式是protocol buffer语法来定义通信数据格式,定义package和service。
  • 然后在server端实现方法,client通过注入拿到实例,直接调用。
Prisma

Typeorm是一个传统的ORM框架,表映射到entity类,把表的关联映射成enttiy类的属性关联。

https://i-blog.csdnimg.cn/direct/f57ae91c7e7f4d0698dcb29041a81473.png

完成entity和表的映射之后,只需要调用userRepositor和postRepository的find,delete,save等api,typeorm会自动生成对应的sql语句并执行。

这就是 ORM (Object Relational Mapping) ,对象和关系型数据库映射。

而Prisma不是这样的。他没有entity类的存在。

用法

定义model,

https://i-blog.csdnimg.cn/direct/4f9164a5e75943e3bab7ed89f483ab93.png

https://i-blog.csdnimg.cn/direct/b1e72c59bb0e4f918be37c94677c2856.png

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文件

https://i-blog.csdnimg.cn/direct/a29cc2374e3e4f1ea55855504be3708e.png

创建对应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了。