@taozizzz/tenant-db-router
v0.1.0
Published
Internal multi-tenant database routing package
Readme
@company/db-tenant
内部多租户数据库路由包,把“选库、隔离、连接管理”收敛成平台能力。
适用于「控制库(控制面) + 租户库(数据面)」的架构:请求进来后先确定 tenantId,再按租户元信息路由到对应数据库连接。
能力概览
- 使用 AsyncLocalStorage 管理
TenantContext TenantRegistry提供租户元信息查询与缓存PrismaFactory管理 Prisma Client 缓存、空闲回收、指纹校验TenantDb统一入口:control()/tenant()
安装
# npm
npm i @company/db-tenant
# pnpm
pnpm add @company/db-tenant说明:
- 本包导出的是通用的「租户路由/缓存/连接池」能力,不强绑定 ORM;示例使用 Prisma。
- 构建产物为 CommonJS:
dist/index.js(详见package.json)。
关键概念
TenantContext:一次请求/任务的上下文(至少包含tenantId),通过 AsyncLocalStorage 透传。TenantMeta:租户元信息(status/dbConfig/ 可选fingerprint等),一般来自控制库。- 控制库(Control DB):存租户元信息的“控制面”数据库。
- 租户库(Tenant DB):每个租户自己的“数据面”数据库(可能是一库一租户/一 schema 一租户等)。
快速开始(示例)
import { TenantDb, TenantRegistry, PrismaFactory, tenantContext } from '@company/db-tenant'
import { TenantFingerprintMismatchError } from '@company/db-tenant'
import { PrismaClient } from '@prisma/client'
const controlClient = new PrismaClient({
datasources: { db: { url: process.env.CONTROL_DB_URL } }
})
// 1) 实现租户元信息查询(通常来自控制库)
const registry = new TenantRegistry({
async getTenantMeta(tenantId) {
return controlClient.tenant.findUnique({
where: { tenantId },
select: {
tenantId: true,
status: true,
fingerprint: true,
dbUrl: true
}
}).then((row) =>
row
? {
tenantId: row.tenantId,
status: row.status,
fingerprint: row.fingerprint,
dbConfig: { url: row.dbUrl }
}
: null
)
}
})
// 2) 管理租户 PrismaClient:缓存 / 最大数量 / 空闲回收 / 指纹校验(可选)
const factory = new PrismaFactory({
createClient: (databaseUrl, meta) =>
new PrismaClient({
datasources: { db: { url: databaseUrl } }
}),
validateFingerprint: async (client, meta) => {
const record = await client.tenantFingerprint.findFirst({
select: { fingerprint: true }
})
if (!record || record.fingerprint !== meta.fingerprint) {
// 抛出该错误会原样透传;抛其他错误会被包装成 TenantFingerprintMismatchError
throw new TenantFingerprintMismatchError(meta.tenantId)
}
},
// maxClients: 2000,
// idleTtlMs: 10 * 60 * 1000,
})
const db = new TenantDb(registry, factory, { controlClient })
// 3) 请求入口绑定租户上下文(以 Express/Koa 风格中间件为例)
function withTenantContext(req, res, next) {
const ctx = {
tenantId: req.user.tenantId,
userId: req.user.userId,
requestId: req.id
}
return tenantContext.run(ctx, next)
}
// 4) 业务层访问
async function handler() {
const tenantDb = await db.tenant()
return tenantDb.user.findMany()
}API 速查
tenantContext
tenantContext.run(context, fn):在fn执行链路内绑定上下文。tenantContext.with(patch, fn):在当前上下文基础上叠加字段(常用于补充requestId等)。tenantContext.get():读取当前上下文(可能为空)。tenantContext.require():读取并校验tenantId;缺失时抛TenantContextMissingError。
TenantDb
db.control():返回控制库 client(透传你传入的controlClient)。await db.tenant():按当前上下文的tenantId路由并返回租户 client。
可选项(new TenantDb(..., options)):
allowedStatuses:允许的租户状态,默认['ACTIVE']。contextAccessor:自定义上下文访问器(默认使用tenantContext)。
TenantRegistry
职责:tenantId -> TenantMeta,带缓存与并发去重。
常用方法:
await registry.getTenantMeta(tenantId):获取租户元信息(缓存命中则直接返回)registry.invalidate(tenantId):租户信息变更后主动失效registry.clear():清空缓存
可选项(new TenantRegistry(adapter, options)):
ttlMs:缓存 TTL,默认 5 分钟maxSize:缓存最大条数,默认 1000useStaleOnError:控制库/adapter 异常时是否用过期值兜底,默认truelogger:可选日志接口
PrismaFactory
职责:tenantId -> PrismaClient 缓存,支持空闲回收、最大 client 数量控制、指纹校验。
常用方法:
await factory.getClient(tenantId, meta):获取/创建租户 clientawait factory.closeClient(tenantId):关闭并移除单个租户 clientawait factory.closeAll():关闭并移除全部 client(用于进程退出/优雅停机)await factory.evictIdleClients():按idleTtlMs清理空闲 client(可由定时任务触发)
可选项(new PrismaFactory(options)):
createClient(databaseUrl, meta):创建 client(必填)validateFingerprint(client, meta):指纹校验(可选)maxClients:最大 client 数量,默认 2000idleTtlMs:空闲回收阈值(可选)logger:可选日志接口
错误与排查
本包会抛出以下错误(均带 code 字段,便于统一处理):
TenantContextMissingError:未绑定上下文或缺少tenantIdTenantNotFoundError:控制库找不到租户TenantStatusError:租户状态不在允许列表TenantRegistryUnavailableError:控制库/registry adapter 不可用(可带cause)TenantDbConnectionLimitError:超过maxClientsTenantFingerprintMismatchError:租户库指纹不一致
本地开发 / 构建
npm install
npm run clean
npm run build- 产物输出到
dist/ - 发布/使用侧通常只需要
dist/(本项目files也仅包含dist)
更多说明
更完整的「打包流程 + 使用说明」见:PACKAGING_AND_USAGE.md。
