Node.js 和 PostgreSQL 多租户基础知识
二月会前进吗?不会,但四月可能会。😂
我知道,这是一个很糟糕的玩笑,但我也知道,如果你继续阅读这篇文章,你将学会如何创建自己的基本多租户Node.js和PostgreSQL API的基础知识。
多租户架构是如何运作的?
简单来说,就是在一个共享的基础设施上运行着一套代码库,但每个客户都拥有一个独立的数据库。
以 Jira 为例,它是最流行的在线项目任务管理工具,用于跟踪错误和问题,以及进行运维项目管理。每个组织都有自己的仪表盘,通过自定义子域名访问。A 和 B 可以访问相同的功能,接收相同的更新,但 A 的问题、工单、评论、用户等信息 B 无法访问,反之亦然。Slack
是另一个多租户的例子,其工作方式与 Jira 类似……当然,这里我们主要讨论的是用户、频道、项目经理、通知等等。
何时必须使用多租户?
试想一下,你一直在开发一款很棒的应用程序,它可以作为 SaaS 提供。提供 SaaS 应用程序的方式有很多种,但如果你的软件需要保持数据库隔离,同时又能为每个客户提供相同的功能,那么它就是必需的。
为什么?
多租户应用程序的优势之一在于代码库的可维护性,因为所有客户端的代码始终相同。如果某个客户端报告问题,解决方案将应用于其他 999 个客户端。需要注意的是,如果您输入错误,该错误也会影响所有客户端。至于数据库管理,可能会稍微复杂一些,但只要遵循适当的模式和约定,一切都会顺利进行。数据库管理有多种方法(例如分布式服务器隔离、使用独立数据集的数据库、使用独立模式的数据库、行隔离),每种方法当然都有其优缺点。
你想学习编程吗?
我选择使用独立的数据库作为数据库方法,因为我认为这对这个例子来说更容易,而且,由于 Sequelize 需要大量的配置,所以我改用了 Knex。
我将重点介绍实现多租户 Node.js 和 PostgreSQL 工作流程所需的特定文件。
多租户 Node.js 和 PostgreSQL
创建用于管理租户的通用数据库
CREATE DATABASE tenants_app;
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
uuid VARCHAR(255) UNIQUE NOT NULL,
db_name VARCHAR(100) UNIQUE NOT NULL,
db_username VARCHAR(100),
db_password TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
database.js:建立与主数据库的连接
const knex = require('knex')
const config = {
client: process.env.DB_CLIENT,
connection: {
user: process.env.DB_USER,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_DATABASE,
password: process.env.DB_PASSWORD
}
}
const db = kenx(config)
module.exports = { db, config }
connection-service.js:用于准备租户数据库连接,换句话说,就是用于在正确的数据库中运行查询的连接。
const knex = require('knex')
const { getNamespace } = require('continuation-local-storage')
const { db, config } = require('../config/database') let tenantMapping
const getConfig = (tenant) => {
const { db_username: user, db_name: database, db_password: password } = tenant
return {
...config,
connection: {
...config.connection,
user,
database,
password
}
}
}
const getConnection = () => getNamespace('tenants').get('connection') || null
const bootstrap = async () => {
try {
const tenants = await db
.select('uuid', 'db_name', 'db_username', 'db_password')
.from('tenants')
tenantMapping = tenants.map((tenant) => ({
uuid: tenant.uuid,
connection: knex(getConfig(tenant))
}))
} catch (e) {
console.error(e)
}
}
const getTenantConnection = (uuid) => {
const tenant = tenantMapping.find((tenant) => tenant.uuid === uuid)
if (!tenant) return null
return tenant.connection
}
tenant-service.js:用于为每个新客户端创建数据库,使用相同的数据库结构,并在需要时用于删除数据库。
const Queue = require('bull')
const { db } = require('../config/database')
const migrate = require('../migrations')
const seed = require('../seeders')
const { bootstrap, getTennantConnection } = require('./connection')
const up = async (params) => {
const job = new Queue(
`setting-up-database-${new Date().getTime()}`,
`redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`
)
job.add({ ...params })
job.process(async (job, done) => {
try {
await db.raw(`CREATE ROLE ${params.tenantName} WITH LOGIN;`) // Postgres requires a role or user for each tenant
await db.raw(
`GRANT ${params.tenantName} TO ${process.env.POSTGRES_ROLE};`
) // you need provide permissions to your admin role in order to allow the database administration
await db.raw(`CREATE DATABASE ${params.tenantName};`)
await db.raw(
`GRANT ALL PRIVILEGES ON DATABASE ${params.tenantName} TO ${params.tenantName};`
)
await bootstrap() // refresh tenant connections to include the new one as available
const tenant = getTenantConnection(params.uuid)
await migrate(tenant) // create all tables in the current tenant database
await seed(tenant) // fill tables with dummy data
} catch (e) {
console.error(e)
}
})
}
tenant.js:用于处理列出、创建或删除租户请求的控制器
const { db } = require('../config/database')
const { v4: uuidv4 } = require('uuid')
const generator = require('generate-password')
const slugify = require('slugify')
const { down, up } = require('../services/tenant-service')
// index
const store = async (req, res) => {
const {
body: { organization }
} = req
const tenantName = slugify(organization.toLowerCase(), '_')
const password = generator.generate({ length: 12, numbers: true })
const uuid = uuidv4()
const tenant = {
uuid,
db_name: tenantName,
db_username: tenantName,
db_password: password
}
await db('tenants').insert(tenant)
await up({ tenantName, password, uuid })
return res.formatter.ok({ tenant: { ...tenant } })
}
const destroy = async (req, res) => {
const {
params: { uuid }
} = req
const tenant = await db
.select('db_name', 'db_username', 'uuid')
.where('uuid', uuid)
.from('tenants')
await down({
userName: tenant[0].db_username,
tenantName: tenant[0].db_name,
uuid: tenant[0].uuid
})
await db('tenants').where('uuid', uuid).del()
return res.formatter.ok({ message: 'tenant was deleted successfully' }) }
module.exports = {
// index,
store,
destroy
}
如下面的图片所示,API 现在能够创建多个客户端,共享服务、端点和其他内容,但保持数据库隔离。
太酷了!
是的,多租户 Node.js 和 PostgreSQL 并没有听起来那么复杂。当然,需要考虑很多因素,例如基础设施、CI/CD、最佳实践和软件模式等等,但只要逐一处理,一切都会顺利进行。正如你所看到的,这种架构可以帮助你的业务实现你想要的扩展,因为云本身就是极限,而目前云的扩展能力是无限的。当然,如果你想查看完整的代码,可以点击这里。
更新:
我创建了一个分支来应用这个概念,使用 MySQL 作为数据库,此外,我也会尽快尝试添加对 Mongoose 的支持。
文章来源:https://dev.to/agusrdz/basics-of-multi-tenant-node-js-and-postgresql-30fl
