跳到主要内容

ZenWeb Tenant module

ZenWeb

多租户多数据库支持 —— 多个租户共享数据库连接池,按请求动态切换数据库。

工作原理

传统多租户方案通常为每个租户创建独立的数据库连接池,当租户数量增多时会耗尽连接资源。本模块采用共享连接池策略:

  • 同一台数据库服务器上的所有租户共用一个连接池
  • 获取连接后通过 USE <数据库名> 切换到对应租户的数据库
  • 按服务器维度配置连接池,而非按租户维度
请求进来 → tenantGetter 识别租户 → 从对应服务器的连接池取连接 → USE db_name → 执行查询

安装

npm install @zenweb/tenant

快速开始

编辑项目入口代码 src/index.ts

src/index.ts
import { create } from 'zenweb';
import modTenant, { Tenant } from '@zenweb/tenant';

export const app = create();

// 租户映射表(实际项目中通常从数据库或配置中心读取)
const tenants: Record<string, Tenant> = {
'a.example.com': { server: 'S1', database: 'db_a' },
'b.example.com': { server: 'S1', database: 'db_b' },
'c.example.com': { server: 'S2', database: 'db_c' },
};

app.setup(modTenant({
// 从请求上下文中识别当前租户
tenantGetter: ctx => {
const tenant = tenants[ctx.host];
if (!tenant) throw new Error('租户不存在: ' + ctx.host);
return tenant;
},

// 按数据库服务器配置连接池
pools: {
S1: {
MASTER: {
host: '127.0.0.1',
port: 3306,
user: 'root',
password: '123456',
charset: 'utf8mb4',
timezone: '+08:00',
connectionLimit: 100,
},
},
S2: {
MASTER: {
host: '192.168.1.100',
port: 3306,
user: 'root',
password: 'secret',
charset: 'utf8mb4',
timezone: '+08:00',
connectionLimit: 50,
},
},
},
}));

app.start();

配置说明

tenantGetter

tenantGetter: (ctx: Context) => Tenant | Promise<Tenant>

从请求上下文中确定当前租户,返回包含 serverdatabase 的对象。识别方式由你决定,常见的有:

  • 域名区分: ctx.host
  • 请求头区分: ctx.headers['x-tenant-id']
  • 路径前缀区分: ctx.path.split('/')[1]
  • Token 区分: 解析 JWT 中的租户字段

pools

pools: {
[服务器名称: string]: {
MASTER: PoolOptions; // 必填,读写库
SLAVES?: PoolOptions[]; // 可选,只读从库,可配多个
}
}
  • 服务器名称是自定义的标识符,与 Tenant.server 对应
  • MASTER 是必须配置的读写库
  • SLAVES 是可选的只读从库列表,配置后读操作会自动路由到从库

可选配置

参数说明
bindQueryZenORM 的 bindQuery 方法,传入后可直接使用 Model.find() 等方法
clustermysql2PoolClusterOptions,连接池集群配置

数据库查询

配置完成后,在控制器中通过 ctx.mysql 进行查询:

src/controller/demo.ts
import { Get, Context } from 'zenweb';

export class DemoController {
@Get()
async list(ctx: Context) {
// 默认使用主库(MASTER)
const rows = await ctx.mysql.query('SELECT * FROM users');

// 显式指定主库
const rows2 = await ctx.mysql.of('MASTER').query('SELECT * FROM users');

// 使用从库(未配置从库时自动回退到主库)
const rows3 = await ctx.mysql.of('SLAVE*').query('SELECT * FROM users');

return rows;
}
}

[可选] 集成 ZenORM

安装

# 生产依赖
npm install zenorm

# 开发依赖
npm install --save-dev @zenorm/generate @zenorm/generate-mysql

配置生成脚本

package.jsonscripts 中增加:

package.json
{
"scripts": {
"dbgen": "zenorm-generate .dbgen.js"
}
}

创建文件 .dbgen.js 用于生成数据库结构代码时连接到指定数据库:

.dbgen.js
/** @type {import("@zenorm/generate").GenerateConfig} */
module.exports = {
host: "localhost",
port: 3306,
user: "root",
password: "",
database: "my_database",
outputDir: "./model",
bindQuery: true,
};

运行命令生成数据库结构代码:

npm run dbgen

传入 bindQuery

编辑项目入口代码,将生成的 bindQuery 传入模块配置:

src/index.ts
import { bindQuery } from './model';

app.setup(modTenant({
bindQuery, // 传入后 ORM 模型可直接查询
tenantGetter,
pools,
}));

使用模型查询

src/controller/user.ts
import { Get } from 'zenweb';
import { User } from '../model';

export class UserController {
@Get()
async list() {
return await User.find().all();
}

@Get(':id')
async detail({ id }) {
return await User.find({ id }).get();
}
}