SaaS多租戶架構(gòu)數(shù)據(jù)源動態(tài)切換解決方案
概述
隨著云計算和SaaS(Software as a Service)模型的興起,多租戶系統(tǒng)成為了構(gòu)建靈活、高效應(yīng)用的重要架構(gòu)。在構(gòu)建多租戶SaaS平臺時,數(shù)據(jù)庫方案的選擇直接關(guān)系到數(shù)據(jù)隔離、性能和可擴(kuò)展性。
在SaaS平臺項目中,根據(jù)前端不同的域名查詢不同的數(shù)據(jù)庫,通常涉及到多租戶架構(gòu)的實現(xiàn)。在這種架構(gòu)中,一個應(yīng)用實例可以服務(wù)多個客戶(租戶)【數(shù)據(jù)庫】,每個租戶的數(shù)據(jù)需要隔離存儲。實現(xiàn)這一目標(biāo)的關(guān)鍵技術(shù)之一就是動態(tài)切換數(shù)據(jù)庫連接。
設(shè)計多租戶數(shù)據(jù)模型
在數(shù)據(jù)庫設(shè)計階段,你需要決定數(shù)據(jù)隔離的級別。通常有以下幾種隔離級別:
- 獨立數(shù)據(jù)庫:每個租戶擁有一個獨立的數(shù)據(jù)庫實例。
- 共享數(shù)據(jù)庫,獨立Schema:所有租戶共享同一個數(shù)據(jù)庫,但每個租戶有獨立的Schema。
- 共享數(shù)據(jù)庫,共享Schema,共享數(shù)據(jù)表:所有租戶共享數(shù)據(jù)庫、Schema和數(shù)據(jù)表,但通過租戶ID字段進(jìn)行數(shù)據(jù)隔離。
共享數(shù)據(jù)庫,獨立Schema
"共享數(shù)據(jù)庫,獨立Schema" 是一種在SaaS平臺中實現(xiàn)多租戶架構(gòu)的策略,它在數(shù)據(jù)庫層面上提供了一種折中的數(shù)據(jù)隔離方法。
Oracle數(shù)據(jù)庫:在Oracle中一個數(shù)據(jù)庫可以具有多個用戶,那么一個用戶一般對應(yīng)一個Schema,表都是建立在Schema中的,(可以簡單的理解:在Oracle中一個用戶一套數(shù)據(jù)庫表)
圖片
在 MySQL 中,Schema 和 Database 可以認(rèn)為是相同的概念。在 SQL 語句中,CREATE DATABASE 和 CREATE SCHEMA 基本上是等效的。所以,當(dāng)你創(chuàng)建一個數(shù)據(jù)庫時,你也在事實上創(chuàng)建了一個模式。模式是一個邏輯上的容器,用于組織和管理數(shù)據(jù)庫對象,如表、視圖、存儲過程等。在 MySQL 中,模式和數(shù)據(jù)庫可以互換使用。
共享數(shù)據(jù)庫
在這種模式下,所有的租戶(即SaaS平臺的客戶)共享同一個物理數(shù)據(jù)庫服務(wù)器或數(shù)據(jù)庫實例。這意味著,盡管每個租戶都有自己的數(shù)據(jù),但這些數(shù)據(jù)都存儲在同一個數(shù)據(jù)庫文件或數(shù)據(jù)庫集群中。這樣做的好處是可以減少硬件資源和維護(hù)成本,因為不需要為每個租戶單獨設(shè)置和維護(hù)數(shù)據(jù)庫實例。
獨立Schema
盡管數(shù)據(jù)庫是共享的,但每個租戶都有自己獨立的Schema。Schema是數(shù)據(jù)庫中的一種邏輯分組,它包含了一系列的數(shù)據(jù)庫對象,如表、視圖、索引、存儲過程等。在這個模式下,每個租戶的數(shù)據(jù)都存儲在自己的Schema中,這樣可以保證租戶之間的數(shù)據(jù)邏輯上是隔離的。
例如,假設(shè)有兩個租戶A和B,他們共享同一個數(shù)據(jù)庫"SaaSDB"。在"SaaSDB"中,可以分別為租戶A和租戶B創(chuàng)建兩個Schema(數(shù)據(jù)庫),分別是"SchemaA"和"SchemaB"。租戶A的所有數(shù)據(jù)都存儲在"SchemaA"中,而租戶B的數(shù)據(jù)存儲在"SchemaB"中。
優(yōu)缺點
優(yōu)點
- 資源利用率高:由于所有租戶共享同一個數(shù)據(jù)庫,硬件資源和數(shù)據(jù)庫維護(hù)成本較低。
- 易于管理:數(shù)據(jù)庫管理員只需要管理一個數(shù)據(jù)庫實例,簡化了維護(hù)和升級的工作。
- 隔離性:每個租戶的數(shù)據(jù)存儲在獨立的Schema中,邏輯上實現(xiàn)了數(shù)據(jù)隔離,減少了數(shù)據(jù)交叉污染的風(fēng)險。
缺點
- 隔離性不如獨立數(shù)據(jù)庫:雖然Schema提供了一定程度的隔離,但如果Schema之間存在依賴關(guān)系或需要進(jìn)行復(fù)雜的數(shù)據(jù)操作,隔離性可能不如完全獨立的數(shù)據(jù)庫。
- 性能問題:如果租戶數(shù)量增多,可能會導(dǎo)致數(shù)據(jù)庫性能問題,因為所有租戶都在競爭同一個數(shù)據(jù)庫資源。
總體來說,"共享數(shù)據(jù)庫,獨立Schema" 的模式在SaaS平臺中是一種常見的多租戶數(shù)據(jù)隔離策略,它在資源利用率和數(shù)據(jù)隔離性之間取得了平衡。開發(fā)者需要根據(jù)具體的業(yè)務(wù)需求和預(yù)期的租戶規(guī)模來決定是否采用這種模式。
SaaS多租戶架構(gòu)數(shù)據(jù)庫設(shè)計
重點:在 SQL 語句中,CREATE DATABASE 和 CREATE SCHEMA 基本上是等效的。所以,當(dāng)你創(chuàng)建一個SCHEMA時,就是在一個RDS實例下創(chuàng)建一個數(shù)據(jù)庫DATABASE。
圖片
以newtrain.tinywan.com、hz_newtrain.tinywan.com、bj_newtrain.tinywan.com三個域名為例,每個域名對應(yīng)一個租戶平臺站點,分別對應(yīng)各自的數(shù)據(jù)源數(shù)據(jù)庫newtrain.tinywan.com、hangzhou.tinywan.com、beijing.tinywan.com。
實施方案
域名解析與路由
- 在DNS系統(tǒng)中為每個域名配置A記錄,指向SaaS平臺的服務(wù)器
- 在服務(wù)器上部署Web應(yīng)用,并根據(jù)請求的Host頭部信息,確定租戶身份。
數(shù)據(jù)源配置
- 在應(yīng)用程序的配置文件中,定義每個租戶的數(shù)據(jù)源配置,包括數(shù)據(jù)庫URL、用戶名和密碼
- 可以使用環(huán)境變量或配置中心來動態(tài)加載這些配置。
動態(tài)數(shù)據(jù)源切換
根據(jù)請求的域名或其他標(biāo)識符,動態(tài)確定使用哪個數(shù)據(jù)庫連接。這通常通過中間件、攔截器或全局函數(shù)來實現(xiàn)。
示例:使用PHP實現(xiàn)域名路由中間件
<?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);
}
}
以上根據(jù)前端請求的域名標(biāo)識符x-site-domain,動態(tài)確定使用哪個數(shù)據(jù)庫連接,通過中間件動態(tài)賦予全局請求對象$request->website,后續(xù)就可以使用。
項目應(yīng)用
項目架構(gòu)
項目使用超高性能可擴(kuò)展PHP框架webman。webman是一款基于workerman開發(fā)的高性能HTTP服務(wù)框架。webman用于替代傳統(tǒng)的php-fpm架構(gòu),提供超高性能可擴(kuò)展的HTTP服務(wù)。你可以用webman開發(fā)網(wǎng)站,也可以開發(fā)HTTP接口或者微服務(wù)。
數(shù)據(jù)庫連接使用ThinkORM。ThinkORM是一個基于PHP和PDO的數(shù)據(jù)庫中間層和ORM類庫,之前一直作為ThinkPHP5.*系列的內(nèi)置ORM類,以優(yōu)異的功能和突出的性能著稱,現(xiàn)已經(jīng)支持獨立使用,并作了升級改進(jìn),提供了更優(yōu)秀的性能和開發(fā)體驗,最新版本要求PHP7.1+。
數(shù)據(jù)庫連接中間
示例:域名路由中間件
<?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);
}
}
數(shù)據(jù)庫配置
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'
]
],
];
Model模型使用
BaseModel.php 基礎(chǔ)模型
<?php
/**
* @desc 基礎(chǔ)模型
* @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
{
/**
* 設(shè)置當(dāng)前模型的數(shù)據(jù)庫連接
* @var string
*/
protected $connection;
/**
* BaseModel constructor.
* @param array $data
*/
public function __construct(array $data = [])
{
$this->connection = \request()->website ?? 'train';
parent::__construct($data);
}
}
CityModel.php 公共模型,對應(yīng)數(shù)據(jù)庫表名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
{
/** 數(shù)據(jù)庫配置 */
protected $connection = 'train';
/** 設(shè)置當(dāng)前模型對應(yīng)的完整數(shù)據(jù)表名稱 */
protected $table = 'common_city';
}
公共模型CityModel類里面定義了connection屬性,則該模型操作的時候會自動按照給定的數(shù)據(jù)庫配置進(jìn)行連接,而不是配置文件中設(shè)置的默認(rèn)連接信息.
業(yè)務(wù) MeetingModel.php 會議模型類。對應(yīng)數(shù)據(jù)庫表名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
{
/** 設(shè)置當(dāng)前模型對應(yīng)的完整數(shù)據(jù)表名稱 */
protected $table = 'resty_meeting';
}
業(yè)務(wù)控制器或者服務(wù)使用
<?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類使用
可以調(diào)用Db::connect方法動態(tài)配置數(shù)據(jù)庫連接信息
<?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方法必須在查詢的最開始調(diào)用,而且必須緊跟著調(diào)用查詢方法,否則可能會導(dǎo)致部分查詢失效或者依然使用默認(rèn)的數(shù)據(jù)庫連接。動態(tài)連接數(shù)據(jù)庫的connect方法僅對當(dāng)次查詢有效。這種方式的動態(tài)連接和切換數(shù)據(jù)庫比較方便,經(jīng)常用于多數(shù)據(jù)庫連接的應(yīng)用需求。
動態(tài)連接到目標(biāo)數(shù)據(jù)庫
在SaaS平臺中,如果需要根據(jù)前端傳遞的配置信息動態(tài)連接到目標(biāo)數(shù)據(jù)庫并將數(shù)據(jù)拉取到本地數(shù)據(jù)庫,可以采用以下步驟實現(xiàn)
- 前端傳遞配置信息。前端在用戶操作時,將目標(biāo)數(shù)據(jù)庫的連接信息作為請求參數(shù)發(fā)送到后端。這些配置信息通常包括數(shù)據(jù)庫類型、主機(jī)地址、端口、數(shù)據(jù)庫名、用戶名和密碼等。
- 驗證和解析配置信息。后端接收到配置信息后,首先進(jìn)行驗證,確保其合法性和安全性。解析配置信息,并準(zhǔn)備用于數(shù)據(jù)庫連接的參數(shù)。
- 動態(tài)數(shù)據(jù)源管理。創(chuàng)建一個動態(tài)數(shù)據(jù)源管理器,它可以根據(jù)傳入的配置信息動態(tài)創(chuàng)建數(shù)據(jù)庫連接。
- 數(shù)據(jù)同步。根據(jù)目標(biāo)數(shù)據(jù)庫的連接信息,建立連接并執(zhí)行數(shù)據(jù)查詢操作。然后將查詢結(jié)果同步到本地數(shù)據(jù)庫。這可能涉及到以下步驟:
建立連接:使用動態(tài)數(shù)據(jù)源管理器創(chuàng)建的目標(biāo)數(shù)據(jù)庫連接。
執(zhí)行查詢:在目標(biāo)數(shù)據(jù)庫上執(zhí)行SQL查詢,獲取所需數(shù)據(jù)。
映射數(shù)據(jù):將查詢結(jié)果映射到本地數(shù)據(jù)庫的表結(jié)構(gòu)中。
寫入本地數(shù)據(jù)庫:將映射后的數(shù)據(jù)插入到本地數(shù)據(jù)庫中。
- 異常處理和日志記錄。在整個數(shù)據(jù)同步過程中,需要妥善處理可能出現(xiàn)的異常情況,并記錄相關(guān)操作日志,以便于問題追蹤和系統(tǒng)維護(hù)。
- 安全性考慮
- 加密敏感信息:確保所有的數(shù)據(jù)庫憑證信息在存儲和傳輸過程中都是加密的。
- 權(quán)限控制:確保只有授權(quán)的用戶或服務(wù)才能訪問數(shù)據(jù)同步功能。
- SQL注入防護(hù):對動態(tài)執(zhí)行的SQL進(jìn)行嚴(yán)格的安全檢查,避免SQL注入攻擊。
自定義函數(shù)
函數(shù)配置文件app/functions.php新增函數(shù)dynamic_connect_db()
/**
* @desc: 動態(tài)切換數(shù)據(jù)庫
* @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) {
// 獲取配置參數(shù)
$config = \think\facade\Db::getConfig();
// 配置具體的數(shù)據(jù)庫連接信息
$config['connections'][$name] = $connection;
// 初始化配置參數(shù)
\think\facade\Db::setConfig($config);
// 創(chuàng)建/切換數(shù)據(jù)庫連接查詢
$connect = \think\facade\Db::connect($name);
}
return $connect;
}
動態(tài)使用
調(diào)用自定義函數(shù)dynamic_connect_db()方法動態(tài)數(shù)據(jù)庫連接查詢,這里查詢一個不存在的配置數(shù)據(jù)庫zhejiang 浙江站點。
/**
* @desc: 動態(tài)切換數(shù)據(jù)庫
* @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());
}
在實際應(yīng)用中,數(shù)據(jù)同步操作可能涉及到復(fù)雜的數(shù)據(jù)映射和處理邏輯,需要根據(jù)具體的業(yè)務(wù)需求進(jìn)行設(shè)計和實現(xiàn)。同時,為了保障系統(tǒng)的穩(wěn)定性和性能,可能還需要考慮引入事務(wù)管理、批量處理和異步處理等機(jī)制。