如何在NestJS项目中结合Prisma实现数据库读写分离复制?
实现NestJS + Prisma + MySQL读写分离方案
一、先完成MySQL主从复制配置
主从复制是读写分离的基础,先确保主从库数据同步正常:
主库(Master)配置
- 修改
my.cnf,开启binlog并设置唯一标识:log-bin=mysql-bin server-id=1 binlog-do-db=your_database_name - 重启MySQL,创建复制专用账号并授权:
CREATE USER 'repl_user'@'slave_ip' IDENTIFIED BY 'your_password'; GRANT REPLICATION SLAVE ON *.* TO 'repl_user'@'slave_ip'; - 锁表导出主库数据(避免导出过程中数据变更):
用FLUSH TABLES WITH READ LOCK;mysqldump导出数据库后执行UNLOCK TABLES;解锁。
- 修改
从库(Slave)配置
- 修改
my.cnf,设置唯一标识和中继日志:server-id=2 relay-log=relay-bin - 导入主库的备份数据。
- 配置主库连接信息并启动复制:
CHANGE MASTER TO MASTER_HOST='master_ip', MASTER_USER='repl_user', MASTER_PASSWORD='your_password', MASTER_LOG_FILE='mysql-bin.xxxxxx', MASTER_LOG_POS=xxx; START SLAVE; - 用
SHOW SLAVE STATUS\G检查Slave_IO_Running和Slave_SQL_Running是否为Yes,确认同步正常。
- 修改
二、配置Prisma多数据源
修改schema.prisma,分别配置主库和从库的数据源:
generator client { provider = "prisma-client-js" } datasource db_master { provider = "mysql" url = env("DATABASE_URL_MASTER") } datasource db_slave { provider = "mysql" url = env("DATABASE_URL_SLAVE") }
在.env文件中添加两个数据库连接地址:
DATABASE_URL_MASTER="mysql://user:password@master_host:3306/your_database" DATABASE_URL_SLAVE="mysql://user:password@slave_host:3306/your_database"
重新生成Prisma客户端:
npx prisma generate
三、在NestJS中实现读写分离逻辑
1. 自定义Prisma服务
创建prisma.service.ts,初始化主、从库两个客户端:
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient as PrismaMasterClient } from '@prisma/client/db_master'; import { PrismaClient as PrismaSlaveClient } from '@prisma/client/db_slave'; @Injectable() export class PrismaService implements OnModuleInit, OnModuleDestroy { public readonly master: PrismaMasterClient; public readonly slave: PrismaSlaveClient; constructor() { this.master = new PrismaMasterClient({ log: ['query', 'warn', 'error'], }); this.slave = new PrismaSlaveClient({ log: ['query', 'warn', 'error'], }); } async onModuleInit() { await this.master.$connect(); await this.slave.$connect(); } async onModuleDestroy() { await this.master.$disconnect(); await this.slave.$disconnect(); } }
2. 业务层区分读写操作
在业务服务中,手动指定写操作走主库,读操作走从库:
import { Injectable } from '@nestjs/common'; import { PrismaService } from './prisma.service'; import { User } from '@prisma/client/db_master'; @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} // 读操作:使用从库 async getUsers(): Promise<User[]> { return this.prisma.slave.user.findMany(); } async getUserById(id: number): Promise<User | null> { return this.prisma.slave.user.findUnique({ where: { id } }); } // 写操作:使用主库 async createUser(data: Omit<User, 'id'>): Promise<User> { return this.prisma.master.user.create({ data }); } async updateUser(id: number, data: Partial<User>): Promise<User> { return this.prisma.master.user.update({ where: { id }, data }); } }
3. 进阶:用拦截器自动路由(可选)
如果不想手动指定客户端,可写拦截器根据请求方法自动切换:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { PrismaService } from './prisma.service'; @Injectable() export class PrismaRoutingInterceptor implements NestInterceptor { constructor(private readonly prisma: PrismaService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); // POST/PUT/DELETE/PATCH 走主库,其余走从库 if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(request.method)) { request.prisma = this.prisma.master; } else { request.prisma = this.prisma.slave; } return next.handle(); } }
在模块中全局注册拦截器:
import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { PrismaRoutingInterceptor } from './prisma-routing.interceptor'; import { PrismaService } from './prisma.service'; @Module({ providers: [ PrismaService, { provide: APP_INTERCEPTOR, useClass: PrismaRoutingInterceptor, }, ], }) export class DatabaseModule {}
四、关键注意事项
- 主从延迟:写操作后立即读可能遇到从库未同步的问题,这类场景需强制使用主库读取。
- 从库故障:可配置多从库,在Prisma服务中加入健康检查和故障切换逻辑。
- 权限控制:从库账号仅授予读权限,主库账号授予读写权限,提升安全性。
内容的提问来源于stack exchange,提问作者Rajib Dey
相关产品推荐
相关产品推荐

