随着云计算和SaaS(Software as a Service)模型的兴起,多租户系统成为了构建灵活、高效应用的重要架构。在构建多租户SaaS平台时,数据库方案的选择直接关系到数据隔离、性能和可扩展性。
在SaaS平台项目中,根据前端不同的域名查询不同的数据库,通常涉及到多租户架构的实现。在这种架构中,一个应用实例可以服务多个客户(租户)【数据库】,每个租户的数据需要隔离存储。实现这一目标的关键技术之一就是动态切换数据库连接。
在数据库设计阶段,你需要决定数据隔离的级别。通常有以下几种隔离级别:
"共享数据库,独立Schema" 是一种在SaaS平台中实现多租户架构的策略,它在数据库层面上提供了一种折中的数据隔离方法。
Oracle数据库:在Oracle中一个数据库可以具有多个用户,那么一个用户一般对应一个Schema,表都是建立在Schema中的,(可以简单的理解:在Oracle中一个用户一套数据库表)
图片
在 MySQL 中,Schema 和 Database 可以认为是相同的概念。在 SQL 语句中,CREATE DATABASE 和 CREATE SCHEMA 基本上是等效的。所以,当你创建一个数据库时,你也在事实上创建了一个模式。模式是一个逻辑上的容器,用于组织和管理数据库对象,如表、视图、存储过程等。在 MySQL 中,模式和数据库可以互换使用。
在这种模式下,所有的租户(即SaaS平台的客户)共享同一个物理数据库服务器或数据库实例。这意味着,尽管每个租户都有自己的数据,但这些数据都存储在同一个数据库文件或数据库集群中。这样做的好处是可以减少硬件资源和维护成本,因为不需要为每个租户单独设置和维护数据库实例。
尽管数据库是共享的,但每个租户都有自己独立的Schema。Schema是数据库中的一种逻辑分组,它包含了一系列的数据库对象,如表、视图、索引、存储过程等。在这个模式下,每个租户的数据都存储在自己的Schema中,这样可以保证租户之间的数据逻辑上是隔离的。
例如,假设有两个租户A和B,他们共享同一个数据库"SaaSDB"。在"SaaSDB"中,可以分别为租户A和租户B创建两个Schema(数据库),分别是"SchemaA"和"SchemaB"。租户A的所有数据都存储在"SchemaA"中,而租户B的数据存储在"SchemaB"中。
总体来说,"共享数据库,独立Schema" 的模式在SaaS平台中是一种常见的多租户数据隔离策略,它在资源利用率和数据隔离性之间取得了平衡。开发者需要根据具体的业务需求和预期的租户规模来决定是否采用这种模式。
重点:在 SQL 语句中,CREATE DATABASE 和 CREATE SCHEMA 基本上是等效的。所以,当你创建一个SCHEMA时,就是在一个RDS实例下创建一个数据库DATABASE。
图片
以newtrain.tinywan.com、hz_newtrain.tinywan.com、bj_newtrain.tinywan.com三个域名为例,每个域名对应一个租户平台站点,分别对应各自的数据源数据库newtrain.tinywan.com、hangzhou.tinywan.com、beijing.tinywan.com。
根据请求的域名或其他标识符,动态确定使用哪个数据库连接。这通常通过中间件、拦截器或全局函数来实现。
示例:使用PHP实现域名路由中间件
<?php/** * @desc 域名路由中间件 * @author Tinywan(ShaoBo Wan) * @date 2024/11/20 18:14 */declare(strict_types=1);namespace app/middleware;use app/common/model/SaasModel;use Webman/Http/Request;use Webman/Http/Response;use Webman/MiddlewareInterface;class ConnectionMiddleware implements MiddlewareInterface{ /** * @param Request $request * @param callable $handler * @return Response */ public function process(Request $request, callable $handler): Response { $domain = $request->header()['x-site-domain']?? 'https://newtrain.tinywan.com'; $platform = SaasModel::where('domain', $domain)->field('id, domain, website')->findOrEmpty(); if (!$platform->isEmpty()) { $request->website = $platform['website']; } return $handler($request); }}
以上根据前端请求的域名标识符x-site-domain,动态确定使用哪个数据库连接,通过中间件动态赋予全局请求对象$request->website,后续就可以使用。
项目使用超高性能可扩展PHP框架webman。webman是一款基于workerman开发的高性能HTTP服务框架。webman用于替代传统的php-fpm架构,提供超高性能可扩展的HTTP服务。你可以用webman开发网站,也可以开发HTTP接口或者微服务。
数据库连接使用ThinkORM。ThinkORM是一个基于PHP和PDO的数据库中间层和ORM类库,之前一直作为ThinkPHP5.*系列的内置ORM类,以优异的功能和突出的性能著称,现已经支持独立使用,并作了升级改进,提供了更优秀的性能和开发体验,最新版本要求PHP7.1+。
示例:域名路由中间件
<?php/** * @desc 域名路由中间件 * @author Tinywan(ShaoBo Wan) * @date 2024/11/20 18:14 */declare(strict_types=1);namespace app/middleware;use app/common/model/SaasModel;use Webman/Http/Request;use Webman/Http/Response;use Webman/MiddlewareInterface;class ConnectionMiddleware implements MiddlewareInterface{ /** * @param Request $request * @param callable $handler * @return Response */ public function process(Request $request, callable $handler): Response { $domain = $request->header()['x-site-domain']?? 'https://newtrain.tinywan.com'; $platform = SaasModel::where('domain', $domain)->field('id, domain, website')->findOrEmpty(); if (!$platform->isEmpty()) { $request->website = $platform['website']; } return $handler($request); }}
ThinkORM配置文件:config/thinkorm.php
<?php/** * @desc ThinkORM配置文件 * @author Tinywan(ShaoBo Wan) * @date 2024/11/14 15:14 */declare(strict_types=1);return [ 'default' => 'train', 'connections' => [ 'train' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'newtrain.tinywan.com', 'username' => 'root', 'password' => '123456' ], 'hangzhou' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'hangzhou.tinywan.com', 'username' => 'root', 'password' => '123456' ], 'beijing' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'beijing.tinywan.com', 'username' => 'root', 'password' => '123456' ] ],];
BaseModel.php 基础模型
<?php/** * @desc 基础模型 * @author Tinywan(ShaoBo Wan) * @date 2024/11/2 15:09 */declare(strict_types=1);namespace app/common/model;use think/Model;class BaseModel extends Model{ /** * 设置当前模型的数据库连接 * @var string */ protected $connection; /** * BaseModel constructor. * @param array $data */ public function __construct(array $data = []) { $this->connection = /request()->website ?? 'train'; parent::__construct($data); }}
CityModel.php 公共模型,对应数据库表名common_city。
<?php/** * @desc 市模型 * @author Tinywan(ShaoBo Wan) * @date 2024/12/13 14:59 */declare(strict_types=1);namespace app/common/model;use think/Model;class CityModel extends Model{ /** 数据库配置 */ protected $connection = 'train'; /** 设置当前模型对应的完整数据表名称 */ protected $table = 'common_city';}
公共模型CityModel类里面定义了connection属性,则该模型操作的时候会自动按照给定的数据库配置进行连接,而不是配置文件中设置的默认连接信息.
业务 MeetingModel.php 会议模型类。对应数据库表名resty_meeting
<?php/** * @desc 会议模型类 * @author Tinywan(ShaoBo Wan) * @date 2024/11/17 11:55 */declare(strict_types=1);namespace app/common/model;class MeetingModel extends BaseModel{ /** 设置当前模型对应的完整数据表名称 */ protected $table = 'resty_meeting';}
业务控制器或者服务使用
<?php/** * @desc 会议 * @author Tinywan(ShaoBo Wan) * @date 2023/11/9 16:57 */declare(strict_types=1);public function meetingList(/support/Request $request, int $organizationId) : /support/Response{ $meetingList = /app/common/model/MeetingModel::where([ 'organization_id' => $organizationId, 'create_user_id' => $this->userId ])->select(); return json($meetingList->toArray());}
可以调用Db::connect方法动态配置数据库连接信息
<?php/** * @desc 会议 * @author Tinywan(ShaoBo Wan) * @date 2023/11/9 16:57 */declare(strict_types=1);public function datasetList(/support/Request $request) : /support/Response{ $res = /think/facade/Db::connect(/request()->website) ->table('resty_meeting') ->field('id,name') ->select(); return json($res->toArray());}
connect方法必须在查询的最开始调用,而且必须紧跟着调用查询方法,否则可能会导致部分查询失效或者依然使用默认的数据库连接。动态连接数据库的connect方法仅对当次查询有效。这种方式的动态连接和切换数据库比较方便,经常用于多数据库连接的应用需求。
在SaaS平台中,如果需要根据前端传递的配置信息动态连接到目标数据库并将数据拉取到本地数据库,可以采用以下步骤实现
建立连接:使用动态数据源管理器创建的目标数据库连接。
执行查询:在目标数据库上执行SQL查询,获取所需数据。
映射数据:将查询结果映射到本地数据库的表结构中。
写入本地数据库:将映射后的数据插入到本地数据库中。
加密敏感信息:确保所有的数据库凭证信息在存储和传输过程中都是加密的。
权限控制:确保只有授权的用户或服务才能访问数据同步功能。
SQL注入防护:对动态执行的SQL进行严格的安全检查,避免SQL注入攻击。
函数配置文件app/functions.php新增函数dynamic_connect_db()
/** * @desc: 动态切换数据库 * @param string $name * @param array $connection * @return /think/db/ConnectionInterface * @author Tinywan(ShaoBo Wan) */function dynamic_connect_db(string $name, array $connection): /think/db/ConnectionInterface{ try { $connect = /think/facade/Db::connect($name); } catch (/Throwable $e) { // 获取配置参数 $config = /think/facade/Db::getConfig(); // 配置具体的数据库连接信息 $config['connections'][$name] = $connection; // 初始化配置参数 /think/facade/Db::setConfig($config); // 创建/切换数据库连接查询 $connect = /think/facade/Db::connect($name); } return $connect;}
调用自定义函数dynamic_connect_db()方法动态数据库连接查询,这里查询一个不存在的配置数据库zhejiang 浙江站点。
/** * @desc: 动态切换数据库 * @param Request $request * @return Response * @throws DataNotFoundException * @throws DbException * @throws ModelNotFoundException * @author Tinywan(ShaoBo Wan) */public function dynamicConnectDb(Request $request): Response{ $connection = [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'zhejiang.tinywan.com', 'username' => 'root', 'password' => '123456' ]; $connect = dynamic_connect_db('zhejiang', $connection); $result = $connect->table('resty_meeting')->where('id', 1)->find(); var_dump($result); return json($result->toArray());}
在实际应用中,数据同步操作可能涉及到复杂的数据映射和处理逻辑,需要根据具体的业务需求进行设计和实现。同时,为了保障系统的稳定性和性能,可能还需要考虑引入事务管理、批量处理和异步处理等机制。
本文链接:http://www.28at.com/showinfo-26-80200-0.htmlSaaS多租户架构数据源动态切换解决方案
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com