在Nodejs中构建多租户建筑的指南
#教程 #node #体系结构 #learning

问候,开发人员!希望你们都过得很好。今天,我很高兴深入研究多租户建筑的领域,并分享我的见解和经验,从构建一个充分采用这种方法的后端应用程序。

让我们画一张生动的图片:想象一下您的软件是一个巨大的公寓大楼。在这个虚拟建筑物中,每个用户或组织都可以作为房客,居住在其中一间公寓中。他们在相同的建筑结构中和平并存,这代表了您软件的后端。但是,他们每个人都保持着独特的私人空间 - 将它们视为在单独数据库中容纳的数据保护区。主要目标?维护他们的数据,确保其安全性,并执行严格的界限,以防止任何未经授权的窥视到其数字“公寓”。这类似于策划庞大的相互联系的数字公寓大楼的管理!

在此博客中,我们将揭开多租户体系结构的复杂性,甚至可以访问初学者。因此,让我们一起踏上这一启发性旅程!

在我们开始之前,我想向这个杰出的Medium博客伸出一个特别的喊叫,这是我探索的灵感。

手头的挑战

  1. 每个租户必须具有其专用数据库。
  2. 管理员应具有停用特定租户的能力。
  3. 在主数据库中存储用户密码是绝对的。

当然,还有其他要求,但是我们将关注这些博客范围的关注点。

高级概述

Multi tenant architecture overview

您可以看到,我们有四个客户,每个客户都有自己的数据库。它们都与同一后端进行互动,该后端执行其魔术并将其与各自的数据库联系起来。为了管理所有这些,我们需要一个超级管理数据库,该数据库可以跟踪所有用户,并且可以存储您可能需要的其他详细信息,例如定价信息或租户可以访问的模块。

现在我们已经设定了舞台,让我们深入研究令人兴奋的部分 -

在此示例中,我将在数据库中使用node.js,express和mongodb。但是,您可以调整这种方法以适合您的技术堆栈。一旦您掌握了这个概念,这是令人惊讶的简单明了的。

P.S-完成博客后,我注意到博客变得太技术性了,有些人可能不喜欢它,只会访问以获取要点或对它的工作原理的想法。因此,我终于添加了一个section,以提供应用程序概述。您可以使用它来获得想法。

代码

初始化

要启动我们的项目,让我们为其创建一个目录。我将其命名为“多租户”,但请随时选择适合您项目的名称。

mkdir multi-tenant

之后,我们初始化了NPM项目

npm init -y

现在,让我们安装必要的第三方软件包。

npm i cookie-parser express jsonwebtoken lru-cache mongoose

也可以随意初始化GIT存储库,但是对于此演示,我不会介绍该步骤。

此外,我将使用ES6模块而不是commonj。请忍受我,因为我在前端和后端工作时,我更喜欢在可能的情况下保持一致。要启用ES6模块,您可以在软件包中添加“类型”:“模块”或使用.mjs文件扩展名。

目录结构

在我们深入探讨代码之前,让我解释一下如何将后端项目组织到不同的目录中以保持代码库的组织。这个博客我们将遵循的结构:

  1. 控制器 - 控制器:这是API输入点所在的位置,主要用于请求验证。
  2. 服务 - 在这里,后端的核心业务逻辑。它包括数据操作,API调用和数据库查询构建。
  3. 存储库 - 这些与数据库进行交互。构建查询后,服务调用存储库,并且存储库处理数据库交互并返回结果。
  4. utils-在此目录中,我存储了不与客户互动但协助后端的帮助助手功能,例如密码哈希,JWT管理等。
  5. 中间件 - 该目录包含在达到我们的API之前执行的代码。在这里,我们可以编写逻辑以确定使用和验证租户的数据库。这种分离有助于使我们的主要后端逻辑与连接逻辑分开。
  6. 服务器 - 它保留了我们服务器的配置文件,例如Express设置和潜在的CORS配置(在此演示中不使用,但建议使用)。
  7. 路由 - 此目录中的代码定义了客户端可以访问的路由和端点。
  8. 模式 - 在这里,我们存储数据库模式。
.
├── controllers
├── middleware
├── repositories
├── routes
├── schema
├── server
├── services
└── utils

这是我们的目录的外观。

现在,请记住,我不会深入了解该演示中的管理员和租户逻辑,但是您可以通过在所有目录中创建单独的管理员和租户目录来实现它,以相应地组织逻辑。对于此演示,我们专注于设置体系结构。

创建我们的服务器

我们将从创建服务器的输入点开始,即index.js文件。它将初始化Express应用程序。我们的Express应用程序的实际初始化将在服务器目录中处理,因为index.js不必担心Express设置。它的作用是需要所有连接到DB,初始化中间件,设置Redis等的初始化功能。

这是server/express.config.js文件:

import express from "express";

const ExpressConfig = () => {
  const app = express();
  app.use(express.json());
  app.set("trust proxy", true);
  return app;
};
export default ExpressConfig;

现在,让我们在我们的index.js中使用此配置,该配置将使我们的服务器绑定到端口:

import ExpressConfig from "./server/express.config.js";

const app = ExpressConfig();


const PORT = 5000;

app.listen(PORT, async () => {
  console.log(`Multi Tenant Backend running on port ${PORT}`);
});

重要的是要注意,对于此演示,我没有使用环境变量来定义端口。但是,在您的生产项目中,强烈建议使用环境变量。这使您可以根据环境动态设置PORT变量和其他配置选项,从而使应用程序更加灵活和安全。

初始中间件设置

现在,让我们潜入为我们的后端设置初始中间件。当前,我们将实现基本中间件,当我们需要编码逻辑以验证和确定数据库连接时,我们将重新访问本节。

我们首先创建middleware/index.js文件

import cors from "cors";
import cookieParser from "cookie-parser";

export default function (app: Application) {

  app.use(cookieParser());

}

对于此演示,我们保持简单。我们正在使用cookie-parser作为仅HTTP的cookie来处理令牌。在生产代码中,您可以在此文件上进行扩展,以包括其他中间件,例如定义CORS策略,速率限制,设置请求上下文(用于记录请求响应周期的文件)等。请随时根据您项目的特定要求和安全考虑。

数据库架构设置

现在,让我们为数据定义架构。在此演示中,我将保持简单,涵盖基本元素,但请随时修改以适合您的特定要求。值得注意的是,我在这里没有在mongoose.model注册我的收藏。当我开始在数据库连接设置上工作时,我将注册它们。背后的原因是避免为租户注册超级管理模式,反之亦然。我们想将这些模式分开。

让我们从超级管理集合开始。

租户收藏

我要构建的第一个集合是租户收藏。在此集合中,您可以存储与租户相关的元数据,例如他们购买的模块,用户限制以及它们是否已启用或禁用。为简单起见,我只存储名称和数据库URI。这是schema/tenant.js的代码:

import {  Schema } from "mongoose";

const tenantSchema = new Schema(
  {
    dbUri: { type: String, required: true },
    name: { type: String, unique: true, required: true },
  }
);

export default tenantSchema;

租户用户收集

第二个超级管理员集合存储了我们应用程序所拥有的所有用户,无论其租户如何。同样,为简单起见,我将保持基础。您可以将其扩展到包括用户角色之类的字段,或者它们是否是租户管理员。这是schema/tenantUser.js的代码:

import { Schema,Types } from "mongoose";

const tenantUserSchema = new Schema(
 {
   email: String,
   tenantId:{
       type: Types.ObjectId,
       ref: "tenants",
    }
  }
);
export default tenantUserSchema;

现在,租户数据库将拥有 -

用户收集

第三个系列是租户收藏。这将存储与该特定租户用户相关的数据。我将其保留在演示中,但是您可以根据需要在图架上添加相关字段。这是schema/users.js的代码:

import { Schema } from "mongoose";


const usersSchema= new Schema(
  {
    email: { type: String, unique: true, required: true },
    password: { type: String },
  }
);
export default usersSchema;

有了这些数据库架构定义,您现在对项目结构以及如何组织数据有一个一般的了解。随意适应并扩展这些模式以符合您项目的特定要求。

存储库

在本节中,我们将定义简单的功能,以进行查询并使用它执行数据库操作。请注意,这些函数的第一个参数将始终是数据库连接对象。由于我们正在处理多租户设置中的多个数据库连接,因此我们需要提供适当的数据库连接到存储库,以便它可以在正确的数据库上执行操作。

让我们从repositories/tenant.js文件开始:

import mongoose from "mongoose";

const mainSchemaName = "tenants";

const getTenantsRepo = async (
  adminDbConnection,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await adminDbConnection
    .model(mainSchemaName)
    .find(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

const getATenantRepo = async (
  adminDbConnection,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await adminDbConnection
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

// This function is part of a service
// that involves many database calls, 
// so we'll use transactions here.
const addATenantRepo = async (
  adminDbConnection,
  tenantData,
  session = null
) => {
  const sessionOption = {};
  if (session) sessionOption.session = session;
  const data = await adminDbConnection
    .model(mainSchemaName)
    .create([tenantData], sessionOption);

  return data[0];
};

const updateATenant = async (
  adminDbConnection,
  findQuery = {},
  updateQuery = {},
) => {

  const data = await adminDbConnection
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

export { getTenantsRepo, getATenantRepo, addATenantRepo, updateATenant };

现在,让我们继续转到repositories/tenantUser.js文件:


const mainSchemaName = "tenantusers";

// This function is part of a service 
// with transactions.
const addATenantUserRepo = async (
  dbConn,
  userData,
  session = null
) => {
  const sessionOption = {};
  if (session) {
    sessionOption.session = session;
  }
  const data = await dbConn
    .model(mainSchemaName)
    .create([userData], sessionOption);
  return data[0];
};

const getATenantUserRepo = async (
  dbConn,
  findQuery,
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();

  return data;
};

const updateATenantUserRepo = async (
  dbConn,
  findQuery,
  updateQuery
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

export { addATenantUserRepo, getATenantUserRepo, updateATenantUserRepo };

最后,我们有repositories/users.js文件:


const mainSchemaName = "users"

// This function is part of the service 
// with transactions.
const addAUserRepo = async (
  dbConn,
  userData,
  session = null
) => {
  const sessionOption = {};
  if (session) sessionOption.session = session;
  const data = await dbConn
    .model(mainSchemaName)
    .create([userData], sessionOption);
  return data[0];
};

const getAUserRepo = async (
  dbConn,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .findOne(findQuery)
    .select(selectQuery)
    .lean();
  return data;
};

const updateUserRepo = async (
  dbConn,
  findQuery,
  updateQuery
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .updateOne(findQuery, updateQuery);
  return data;
};

const getUsersRepo = async (
  dbConn,
  findQuery = {},
  selectQuery = {}
) => {
  const data = await dbConn
    .model(mainSchemaName)
    .find(findQuery)
    .select(selectQuery)
    .lean();

  return data;
};


export {
  addAUserRepo,
  getAUserRepo,
  updateUserRepo,
  getUsersRepo,
};

主连接设置

在本节中,我们将重点介绍用于管理所有数据库连接的逻辑,并利用最佳数据库结构(例如LRU(最不使用最近)缓存)来有效地管理这些连接。当我们在此处定义管理和租户连接的两个初始化功能时,您应该考虑将逻辑分开以供更好的组织。此外,请注意,我们在数据库对象上注册模型。

让我们从utils/initDBConnection.js文件开始

import mongoose, { Connection } from "mongoose";
import TenantSchema from "../schema/tenant.js";
import TenantUserSchema from "../schema/tenantUser.js";
import UserSchema from "../schema/user.js";

const clientOption = {
  socketTimeoutMS: 30000,
  useNewUrlParser: true,
  useUnifiedTopology: true,
};

// Log MongoDB queries
mongoose.set("debug", true);

const initAdminDbConnection = async (
  DB_URL
) => {
  try {
    const db = mongoose.createConnection(DB_URL, clientOption);

    db.on("error", (err) =>
      console.log("Admin db error: ", err)
    );

    db.once("open", () => {
      console.log("Admin client MongoDB Connection ok!");
    });

    await db.model("tenants", TenantSchema);
    await db.model(
      "tenantusers",
      TenantUserSchema
    );

    return db;
  } catch (error) {
      return error;
  }
};

const initTenantDBConnection = async (
  DB_URL,
  dbName
) => {
  try {
    const db = mongoose.createConnection(DB_URL, clientOption);

    db.on("error", (err) =>
      console.log(`Tenant ${dbName} db error: `, err)
    );

    db.once("open", () => {
      console.log(
        `Tenant connection for ${dbName} MongoDB Connection ok!`
      );
    });

    await db.model("users", UserSchema);

    return db;
  } catch (error) {
    return error;
  }
};
export { initAdminDbConnection, initTenantDBConnection }

现在,让我们在utils/lruCacheManager.js文件中定义LRU缓存管理器的代码:

import { LRUCache } from "lru-cache";
import { Connection } from "mongoose";

const cacheOptions = {
  max: 5000,
  maxAge: 1000 * 60 * 60,
};

const connectionCache = new LRUCache(cacheOptions);

const setCacheConnection = (tenantId, dbConn): void => {
  console.log("Setting connection cache for ", tenantId);
  connectionCache.set(tenantId, dbConn);
};

const getCacheConnection = (tenantId) => {
  return connectionCache.get(tenantId);
};

const getCacheValuesArr = () => {
  return connectionCache.values();
};

export { setCacheConnection, getCacheConnection, getCacheValuesArr };

现在,让我们编码是应用程序核心的文件,即连接管理器文件!该文件包含用于初始化数据库和管理集合的逻辑,以便我们的应用程序可以使用它们。这是utils/connectionManager.js的代码:

import mongoose from "mongoose";

import { initAdminDbConnection, initTenantDBConnection } from "./initDBConnection.js";
import {
  getATenantRepo,
  getTenantsRepo,
} from "../repositories/tenant.js";
import {
  getCacheConnection,
  getCacheValuesArr,
  setCacheConnection,
} from "./lruCacheManager.js";

let adminDbConnection;

// This function will be called at the start
// of our server. Its purpose is to initialize the admin database
// and the database connections for all of the tenants.
export const connectAllDb = async () => {
  const ADMIN_DB_URI = `your admin db uri`;

  adminDbConnection = await initAdminDbConnection(ADMIN_DB_URI);

  const allTenants = await getTenantsRepo(
    adminDbConnection,
    { name: 1, dbUri: 1, _id: 1 }
  );

  for (const tenant of allTenants) {
    const tenantConnection = await initTenantDBConnection(
      tenant.dbUri,
      tenant.name
    );

    setCacheConnection(tenant._id.toString(), tenantConnection);
  }
};

export const getConnectionForTenant = async (
  tenantId
) => {
  console.log(`Getting connection from cache for ${tenantId}`);
  let connection = getCacheConnection(tenantId);
  if (!connection) {
    console.log(`Connection cache miss for ${tenantId}`);

    const tenantData = await getATenantRepo(
        adminDbConnection,
        { _id: tenantId },
        { dbUri: 1, name: 1 }
      )

    if (tenantData) {
      connection = await initTenantDBConnection(
        tenantData.dbUri,
        tenantData.name
      );
      if (!connection) return null;

      console.log("Connection cache added for ", tenantData.name);

    } else {
      console.log(
        "No connection data for tenant with ID",
        tenantId
      );
      return null;
    }
  }

  return connection;
};

export const getAdminConnection = () => {
  console.log("Getting adminDbConnection");
  return adminDbConnection;
};

const gracefulShutdown = async () => {
  console.log("Closing all database connections...");

  const connectionArr = getCacheValuesArr();

  // Close all tenant database connections from the cache
  for (const connection of connectionArr) {
      await connection.close();
      console.log("Tenant database connection closed.");
  }

  // Close the admin database connection if it exists
  if (adminDbConnection) {
    await adminDbConnection.close();
    console.log("Admin database connection closed.");
  }

  console.log("All database connections closed. Yay!");
};

let isShutdownInProgress = false;

// Listen for termination signals
["SIGINT", "SIGTERM", "SIGQUIT", "SIGUSR2"].forEach((signal) => {
  process.on(signal, async () => {
    if (!isShutdownInProgress) {
      console.log(`Received ${signal}, gracefully shutting down...`);
      isShutdownInProgress = true;
      await gracefulShutdown();
      process.exit(0);
    }
  });
});

哈希!!!这是我们申请的主要部分。希望您了解该代码不仅仅是管理DB连接的功能,但它仍然是我们应用程序的核心。

让我们定义一些UTIT功能,这些功能将在以后的服务中帮助我们。我们可以将此文件命名为包含其他功能的杂项JS,因此这是utils/misc.js的代码

import jwt from "jsonwebtoken";

const signJWT = (data) => {

  return jwt.sign(data, "random secret");

};

const verifyJWT = (
  payload
) => {
    return jwt.verify(payload, "random secret");
};

// define in your env file
const saltRounds = 10

const generateHash = async (input) => {
  try {
    const hash = await bcrypt.hash(input, Number(saltRounds));
    return hash;
  } catch (error) {
    console.error("Error generating hash:", error);
    throw error;
  }
};

const comparePassword = async (plainPassword, hash) => {
  try {
    const match = await bcrypt.compare(plainPassword, hash);
    return match;
  } catch (error) {
    console.error("Error comparing password:", error);
    throw error;
  }
};

export {
  signJWT,
  verifyJWT,
  generateHash,
  comparePassword,
};

服务

在本节中,我们将研究应用程序的核心业务逻辑。

因此,让我们从最激烈的服务文件开始! services/tenant.js文件。

import mongoose from "mongoose";
import {
  addATenantRepo,
 } from "../repositories/tenant.js";
import { addATenantUserRepo } from "../repositories/tenantUser.js";
import { setCacheConnection } from "../utils/lruCacheManager.js";

import { addAUserRepo } from "../repositories/user.js";
import { initAdminDbConnection, initTenantDBConnection } from "../utils/initDBConnection.js";

const addATenantService = async (
  dbConn,
  tenantData
) => {
  const session = await dbConn.startSession();
  session.startTransaction();
  try {
    const data = await addATenantRepo(
      dbConn,
      { ...tenantData },
      session
    );

    let userData;
    if (data._id) {
      userData = await addATenantUserRepo(
        dbConn,
        {
          tenantId: data._id,
          email: tenantData.email,
        },
        session
      );

      const tenantDbConnection = await initTenantDBConnection(
        data.dbUri,
        data.name
      );

      await addAUserRepo(
        tenantDbConnection,
        {
          _id: userData._id,
          email: tenantData.email,
        },
        session
      );

      await session.commitTransaction();
      session.endSession();

      setCacheConnection(data._id.toString(), tenantDbConnection);
    }

    return {
      success: true,
      statusCode: 201,
      message: `Tenant added successfully`,
      responseObject: { tenantId: data._id, userId: userData?._id },
    };
  } catch (error) {
    await session.abortTransaction();
    session.endSession();
    throw error;
  }
};

export { addATenantService };

AddatenantService功能处理在系统中添加新租户的过程。它遵循以下步骤:

  1. 将租户数据添加到超级管理员租户收藏中。
  2. 将用户详细信息添加到超级管理员租户用户收藏中。
  3. 将用户链接到租户用户集合中的租户,保持超级管理员和租户数据库之间的一致性。

现在,让我们转到包含与身份验证相关的逻辑的services/auth.js文件:

import { signJWT } from "../utils/misc.js";

const loginService = async (
  userData
) => {
  if (!userData || !userData|| !userData._id || !userData.tenantId)
    return {
      success: false,
      statusCode: 401,
      message: `No user with the given credentials`,
      responseObject: {
        incorrectField: "email",
      },
    };

  // Do some password matching

  const accessToken = signJWT(
    {
      userId: userData._id.toString(),
      tenantId: userData.tenantId.toString(),
    }
  );

  return {
    success: true,
    statusCode: 200,
    message: `Logged In Successfully`,
    responseObject: {
      accessToken,
      userId: userData._id.toString(),
      tenantId: userData.tenantId.toString(),
    },
  };
};

LoginService函数处理用户登录并生成访问令牌,这对于我们的应用程序至关重要,因为它包含用户ID和tenantid。该令牌将用于验证请求并基于tatantid确定适当的数据库连接。对于此演示,我没有编写代码以匹配密码,但是您应该根据自己的需求编写自己的逻辑。

这是我们的服务部分。现在,让我们继续进入控制器和路线部分。

控制器和路线

我已经介绍了服务部分,现在让我们深入研究控制器和路线。在本节中,我将为您提供设置控制器和路线的必要代码。由于代码很简单,我不会深入研究详细的解释。但是,有效地组织您的代码至关重要,因此我将提供一个基本的结构。

所以这是我们的controllers/index.js文件代码

import { loginService } from "../services/auth.js"
import { addATenantService } from "../services/tenant.js"

export function loginController = async (req,res)=>{
  const serviceFnResponse = await loginService(req.body);

  res.status(serviceFnResponse.code).json({...serviceFnResponse});
}

export function addATenantController = async (req,res)=>{
  const serviceFnResponse = await addATenantService(req.body);

  res.status(serviceFnResponse.code).json({...serviceFnResponse});
}

在这些控制器中,您应该添加代码以验证请求主体,以确保数据完整性和安全性。适当的输入验证是构建强大应用程序的关键步骤。

现在,让我们在routes/index.js文件中定义您的应用程序的路由:

import { Router } from "express"
import { loginController, addATenantController } from "../controllers/index.js"

const router = Router()

router.post("/add",addATenantController);
router.post("/login",loginController);

export default router;

此代码创建一个快速路由器并定义您的应用程序路由。在实际情况下,您可能会有更多路线,每条路线都映射到特定的控制器。为了维护性,请考虑将路线组织到单独的文件中,每个资源或功能一个。

通过以这种方式构造代码,您可以维护一个干净有条理的项目,从而更容易添加新功能或扩展现有功能。

中间件

现在,让我们潜入中间软件的引擎。该代码将处理繁重的工作,例如确定每个请求的数据库连接和租户。我们将从middleware/databaseResolver.js文件开始:

import { getConnectionForTenant } from "../utils/connectionManager.js";
import { verifyJWT } from "../utils/misc.js";

export const databaseResolver = async (req, _, next) => {
  const urlArr = req.url.split("/");

  // Skip database resolution for login route
  if (urlArr.includes("login")) return next();

  const token = req.headers.jwt;
  // Handle the logic for null checking and authorization
  const payloadData = verifyJWT(token);
  // Handle the expiry logic, etc.
  const dbConnection = getConnectionForTenant(payload.tenantId);

  // Here, we are directly populating the req object, but you can use
  // custom context managers in your application
  req.dbConnection = dbConnection;
  next();
};

我们定义了基本的中间件逻辑,该逻辑可以根据请求的租户信息解决适当的数据库连接。中间件还跳过了登录路线的此过程。

接下来,让我们配置要在应用程序中使用的中间件。创建一个名为server/middleware.config.js的新文件:

import { databaseResolver } from "../middleware/databaseResolver.js"

export default function(app){
  app.use(databaseResolver);
}

您可能想知道为什么当代码相对较短时,我们为什么使用一个单独的文件进行中间件。但是,在生产级别的应用程序中,您可能会拥有更复杂的中间件要求,例如请求消毒,记录等等。拥有单独的中间件配置文件,您可以有效地管理和组织这些要求。
exmaple-秘密化中间件,用于请求,记录中间件等。

此文件设置了您的应用程序的路由,在现实情况下,您将通过资源或功能组织多个路由。

server/route.config.js

import router from "routes/index.js"

export default function(app){
  app.use('/api',router);
}

现在您需要做的就是需要索引中的所有配置

import ExpressConfig from "./server/express.config.js";
import MiddlewareConfig from "./server/middleware.config.js";
import RouteConfig from "./server/route.config.js";

const app = ExpressConfig();

MiddlewareConfig(app)
RouteConfig(app)

const PORT = 5000;

app.listen(PORT, async () => {
  console.log(`Multi Tenant Backend running on port ${PORT}`);
});

通过以这种方式构造代码,您可以维护一个干净且有条理的索引。

我们完成了!祝贺您为自己的多租户申请建立基础。这种强大的结构可以用作创建复杂软件作为服务(SaaS)应用程序的基础。如果您觉得技术细节变得有些不知所措,请不用担心。让我们使用图表可视化应用程序的工作原理,为我们的应用程序中的所有活动部件提供了进修。

应用概述

Application overview

  1. 客户端请求:当用户与您的平台进行交互(例如登录或访问数据)时,他们的请求将发送到您的服务器。此请求带有一个特殊密钥,称为JSON Web令牌(JWT),例如秘密密码。

  2. 中间件:请求首先输入一个称为中间件的检查点。就像俱乐部的弹跳者一样。如果请求是关于登录的,则可以快速通过。否则,它将进入下一步。

  3. 数据库解析器:这是真正的魔术。数据库解析器查看JWT密码,并算出用户属于哪个公司(房客)。这就像将邮件分类到不同的邮箱。

  4. 数据库连接:一旦解析器知道它是哪个租户,它将打开右门 - 租户的数据库。好像每个租户在一个巨大的图书馆里都有自己的房间,只有他们才能访问自己的书。

  5. 控制器:正确的房间(数据库)打开,控制器接管了。就像一个图书馆员,可以帮助您找到所需的书。控制器弄清楚用户想做什么,并从正确的房间(数据库)获取信息。

  6. 服务:在控制器内部,称为服务的特殊助手做艰苦的工作。他们执行任务,例如检查用户的密码是否正确或获取数据。这些服务就像专家图书馆员,他们知道每本书的保留位置。

  7. 数据库互动:服务与数据库交谈,提出问题并获得答案。例如,他们可能会问:“这是正确的密码吗?”或“给我这个用​​户的数据”。每个租户的数据都保持分开,因此没有混音。

  8. 响应生成:一旦服务具有所有答案,它们就会产生响应。这就像将所有必要信息的报告汇总在一起一样,其中包括一条诸如“成功!”之类的消息。或“对不起,那是不对的。”

  9. 控制器响应:控制器获取此报告并为交付准备。这就像将其放在用户地址上的信封中。

  10. 客户端响应:最后,响应发送回用户的设备。就像在邮件中获取报告一样。用户看到消息和他们要求的任何数据,他们可以继续使用您的平台。

因此,简单来说,您的多租户应用程序就像一个巨大的库,上面有许多房间(数据库)。当用户索要某些内容时,应用程序确保他们进入正确的房间以获取正确的信息。这样,每个租户的数据都保持私人和安全,就像图书馆不同房间的不同书籍一样。

此系统非常适合运行云服务,每个租户在共享同一平台时都有自己的空间。

如果您还有任何其他问题或需要对应用程序的特定方面进行其他澄清,请随时提出!另外,如果您有任何改进建议,请告诉我我很想听听。