本文目录导读:

我来详细讲解 PHP 单数据库多租户(Single Database Multi-Tenancy)的完整实现方案。
架构概述
基本概念
- 单数据库:所有租户共享同一个数据库
- 区分方式:通过租户ID字段区分数据
- 优点:成本低、维护简单
- 缺点:数据隔离性较差
数据表设计
-- 租户表
CREATE TABLE tenants (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
domain VARCHAR(100) UNIQUE,
status TINYINT DEFAULT 1,
config JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 用户表(带租户ID)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
password_hash VARCHAR(255),
role VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant_user (tenant_id, username),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- 业务数据表(以订单为例)
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL,
order_no VARCHAR(32) UNIQUE,
customer_name VARCHAR(50),
amount DECIMAL(10,2),
status TINYINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant_order (tenant_id, created_at),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
数据库连接类
<?php
// Database.php
class Database {
private static $instances = [];
private static $tenantId = null;
public static function setTenantId($tenantId) {
self::$tenantId = $tenantId;
}
public static function getTenantId() {
return self::$tenantId;
}
public static function connection() {
$config = [
'host' => 'localhost',
'database' => 'saas_db',
'username' => 'root',
'password' => 'password',
'charset' => 'utf8mb4'
];
$key = $config['host'] . $config['database'];
if (!isset(self::$instances[$key])) {
$dsn = sprintf(
"mysql:host=%s;dbname=%s;charset=%s",
$config['host'],
$config['database'],
$config['charset']
);
self::$instances[$key] = new PDO(
$dsn,
$config['username'],
$config['password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
}
return self::$instances[$key];
}
}
基础模型类
<?php
// BaseModel.php
abstract class BaseModel {
protected $table;
protected $db;
protected $tenantId;
public function __construct() {
$this->db = Database::connection();
$this->tenantId = Database::getTenantId();
}
// 自动注入租户条件
protected function applyTenantFilter($query, $alias = '') {
if ($this->tenantId) {
$column = $alias ? "{$alias}.tenant_id" : "tenant_id";
$query .= " AND {$column} = :tenant_id";
}
return $query;
}
// 查询单条记录
public function find($id) {
$sql = "SELECT * FROM {$this->table} WHERE id = :id";
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':id', $id);
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
$stmt->execute();
return $stmt->fetch();
}
// 查询多条记录
public function all($conditions = []) {
$sql = "SELECT * FROM {$this->table} WHERE 1=1";
foreach ($conditions as $key => $value) {
$sql .= " AND {$key} = :{$key}";
}
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
foreach ($conditions as $key => $value) {
$stmt->bindValue(":{$key}", $value);
}
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
$stmt->execute();
return $stmt->fetchAll();
}
// 插入数据
public function create($data) {
$data['tenant_id'] = $this->tenantId;
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
$stmt = $this->db->prepare($sql);
foreach ($data as $key => $value) {
$stmt->bindValue(":{$key}", $value);
}
$stmt->execute();
return $this->db->lastInsertId();
}
// 更新数据
public function update($id, $data) {
$sets = [];
foreach ($data as $key => $value) {
$sets[] = "{$key} = :{$key}";
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $sets) . " WHERE id = :id";
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
foreach ($data as $key => $value) {
$stmt->bindValue(":{$key}", $value);
}
$stmt->bindValue(':id', $id);
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
return $stmt->execute();
}
// 删除数据
public function delete($id) {
$sql = "DELETE FROM {$this->table} WHERE id = :id";
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':id', $id);
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
return $stmt->execute();
}
}
具体模型实现
<?php
// User.php
class User extends BaseModel {
protected $table = 'users';
public function findByUsername($username) {
$sql = "SELECT * FROM {$this->table} WHERE username = :username";
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
$stmt->bindValue(':username', $username);
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
$stmt->execute();
return $stmt->fetch();
}
}
// Order.php
class Order extends BaseModel {
protected $table = 'orders';
// 获取租户的统计数据
public function getStats() {
$sql = "SELECT
COUNT(*) as total_orders,
SUM(amount) as total_amount,
AVG(amount) as avg_amount
FROM {$this->table}";
$sql = $this->applyTenantFilter($sql);
$stmt = $this->db->prepare($sql);
if ($this->tenantId) {
$stmt->bindValue(':tenant_id', $this->tenantId);
}
$stmt->execute();
return $stmt->fetch();
}
// 分页查询
public function paginate($page = 1, $limit = 20, $filters = []) {
$offset = ($page - 1) * $limit;
$where = "WHERE 1=1";
foreach ($filters as $key => $value) {
$where .= " AND {$key} = :{$key}";
}
// 获取总数
$countSql = "SELECT COUNT(*) as total FROM {$this->table} {$where}";
$countSql = $this->applyTenantFilter($countSql);
$countStmt = $this->db->prepare($countSql);
foreach ($filters as $key => $value) {
$countStmt->bindValue(":{$key}", $value);
}
if ($this->tenantId) {
$countStmt->bindValue(':tenant_id', $this->tenantId);
}
$countStmt->execute();
$total = $countStmt->fetch()['total'];
// 获取数据
$dataSql = "SELECT * FROM {$this->table} {$where}
ORDER BY created_at DESC
LIMIT :offset, :limit";
$dataSql = $this->applyTenantFilter($dataSql);
$dataStmt = $this->db->prepare($dataSql);
foreach ($filters as $key => $value) {
$dataStmt->bindValue(":{$key}", $value);
}
if ($this->tenantId) {
$dataStmt->bindValue(':tenant_id', $this->tenantId);
}
$dataStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$dataStmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$dataStmt->execute();
$data = $dataStmt->fetchAll();
return [
'data' => $data,
'total' => $total,
'page' => $page,
'limit' => $limit,
'total_page' => ceil($total / $limit)
];
}
}
租户认证中间件
<?php
// TenantMiddleware.php
class TenantMiddleware {
public static function handle() {
session_start();
// 通过域名识别租户
$domain = $_SERVER['HTTP_HOST'];
$tenant = self::getTenantByDomain($domain);
if (!$tenant) {
http_response_code(404);
exit('Tenant not found');
}
// 设置当前租户ID
Database::setTenantId($tenant['id']);
// 验证登录状态(可选)
if (!self::validateAuth($tenant)) {
header('Location: /login');
exit;
}
}
private static function getTenantByDomain($domain) {
$db = Database::connection();
$stmt = $db->prepare("SELECT * FROM tenants WHERE domain = :domain AND status = 1");
$stmt->execute(['domain' => $domain]);
return $stmt->fetch();
}
private static function validateAuth($tenant) {
// 检查session中的tenant_id是否匹配
return isset($_SESSION['tenant_id']) &&
$_SESSION['tenant_id'] == $tenant['id'];
}
}
控制器示例
<?php
// UserController.php
class UserController {
public function index() {
$userModel = new User();
$users = $userModel->all();
// 返回视图
}
public function show($id) {
$userModel = new User();
$user = $userModel->find($id);
if (!$user) {
return ['error' => 'User not found'];
}
return $user;
}
public function store() {
$data = $_POST;
$rules = [
'username' => 'required|min:3',
'email' => 'required|email',
'password' => 'required|min:6'
];
$validator = new Validator($rules);
if (!$validator->validate($data)) {
return ['errors' => $validator->getErrors()];
}
$userModel = new User();
$userModel->create($data);
return ['success' => true, 'message' => 'User created'];
}
public function update($id) {
$data = $_POST;
$userModel = new User();
$updated = $userModel->update($id, $data);
if (!$updated) {
return ['error' => 'Update failed'];
}
return ['success' => true];
}
public function destroy($id) {
$userModel = new User();
$deleted = $userModel->delete($id);
if (!$deleted) {
return ['error' => 'Delete failed'];
}
return ['success' => true];
}
}
入口文件
<?php
// index.php
require_once 'Database.php';
require_once 'BaseModel.php';
require_once 'TenantMiddleware.php';
require_once 'User.php';
require_once 'Order.php';
require_once 'UserController.php';
// 运行租户认证中间件
TenantMiddleware::handle();
// 简单路由
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
$controller = new UserController();
if ($method == 'GET' && $uri == '/users') {
echo json_encode($controller->index());
} elseif ($method == 'GET' && preg_match('/\/users\/(\d+)/', $uri, $matches)) {
echo json_encode($controller->show($matches[1]));
} elseif ($method == 'POST' && $uri == '/users') {
echo json_encode($controller->store());
} elseif ($method == 'PUT' && preg_match('/\/users\/(\d+)/', $uri, $matches)) {
echo json_encode($controller->update($matches[1]));
} elseif ($method == 'DELETE' && preg_match('/\/users\/(\d+)/', $uri, $matches)) {
echo json_encode($controller->destroy($matches[1]));
}
安全优化建议
数据加密
<?php
// 对敏感字段加密
class DataEncryption {
private static $key = 'your-secret-key';
public static function encrypt($data) {
$method = 'AES-256-CBC';
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($data, $method, self::$key, 0, $iv);
return base64_encode($iv . $encrypted);
}
public static function decrypt($data) {
$data = base64_decode($data);
$method = 'AES-256-CBC';
$iv = substr($data, 0, 16);
$encrypted = substr($data, 16);
return openssl_decrypt($encrypted, $method, self::$key, 0, $iv);
}
}
SQL注入防护
<?php
// 使用预处理语句(已经在BaseModel中实现)
// 额外添加防注入中间件
class SecurityMiddleware {
public static function filterInput($data) {
if (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = self::filterInput($value);
}
} else {
// 移除危险字符
$data = preg_replace('/([\x00-\x08\x0b\x0c\x0e-\x1f])/', '', $data);
}
return $data;
}
}
性能优化
<?php
// 为租户常用的查询添加索引
-- 示例索引
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
CREATE INDEX idx_orders_tenant_date ON orders(tenant_id, created_at);
CREATE INDEX idx_users_tenant_role ON users(tenant_id, role);
// 缓存层
class CacheManager {
private static $cache = [];
public static function get($key) {
$cacheKey = Database::getTenantId() . '_' . $key;
return self::$cache[$cacheKey] ?? null;
}
public static function set($key, $value) {
$cacheKey = Database::getTenantId() . '_' . $key;
self::$cache[$cacheKey] = $value;
}
}
租户管理后台
<?php
// TenancyAdmin.php
class TenancyAdmin {
public function createTenant($name, $domain) {
$db = Database::connection();
$stmt = $db->prepare(
"INSERT INTO tenants (name, domain, status, config)
VALUES (:name, :domain, 1, :config)"
);
$config = json_encode([
'max_users' => 100,
'storage_limit' => '10GB',
'features' => ['basic', 'advanced']
]);
$stmt->execute([
'name' => $name,
'domain' => $domain,
'config' => $config
]);
return $db->lastInsertId();
}
public function deactivateTenant($tenantId) {
$db = Database::connection();
$stmt = $db->prepare(
"UPDATE tenants SET status = 0 WHERE id = :id"
);
return $stmt->execute(['id' => $tenantId]);
}
public function getTenantUsage($tenantId) {
$db = Database::connection();
// 统计用户数
$userCount = $db->query(
"SELECT COUNT(*) as count FROM users WHERE tenant_id = $tenantId"
)->fetch()['count'];
// 统计订单数
$orderCount = $db->query(
"SELECT COUNT(*) as count FROM orders WHERE tenant_id = $tenantId"
)->fetch()['count'];
return [
'users' => $userCount,
'orders' => $orderCount
];
}
}
优点
- 成本低:维护一个数据库,减少基础设施成本
- 易于部署:升级维护相对简单
- 数据备份简单:只需备份一次
缺点
- 数据隔离性差:需要严格的查询条件控制
- 性能瓶颈:数据量大时可能出现性能问题
- 扩展性受限:难以针对个别租户优化
最佳实践
- 始终在查询中加入tenant_id条件
- 使用ORM/模型层统一处理租户逻辑
- 定期备份,确保数据安全
- 监控查询效率,及时优化索引
- 为敏感数据加密,加强安全性
这个方案适合中小型SaaS应用,对于数据量和性能要求不高的情况,如果是大型应用,建议考虑"分库"或"分表"的架构。