Electric多租户设计:Shapes隔离与共享数据策略
引言:多租户架构的隐形挑战
你是否还在为SaaS应用的数据隔离问题头疼?当用户规模从百级跃升至十万级,传统的"一库一租户"方案带来的存储成本激增和运维复杂度已经成为业务增长的绊脚石。Electric作为Postgres同步引擎,通过Shapes技术提供了细粒度的数据隔离与共享方案,让多租户系统在安全性与资源利用率之间找到完美平衡。
读完本文你将掌握:
- 基于Shapes的租户数据隔离实现
- 跨租户数据共享的三种高级模式
- 性能优化与安全加固的最佳实践
- 从0到1部署多租户应用的完整流程
多租户架构的演进与痛点
传统方案的局限性
| 架构模式 | 实现方式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 独立数据库 | 为每个租户分配独立数据库 | 完全隔离,安全级别高 | 资源利用率低,维护成本高 | 金融、医疗等强监管行业 |
| 共享数据库独立Schema | 同一数据库不同Schema | 中等隔离,运维成本较低 | 备份恢复复杂,存在资源竞争 | 中小型SaaS应用 |
| 共享数据库共享Schema | 所有租户数据在同一表中 | 资源利用率最高,扩展灵活 | 隔离性差,查询复杂度高 | 超大规模SaaS平台 |
Electric的革新性解决方案
Electric通过逻辑隔离而非物理隔离的方式,结合Postgres的行级安全与Shapes的部分复制能力,实现了"共享基础设施,隔离数据视图"的新型多租户架构。其核心优势在于:
- 细粒度控制:可按租户、用户甚至功能模块定义数据可见性
- 动态扩展:无需重启服务即可调整租户数据范围
- 低资源消耗:避免独立数据库带来的存储与计算冗余
- 实时同步:毫秒级数据一致性保障
Shapes隔离机制:租户数据边界的守护者
Shapes核心概念
Shape(形状)是Electric实现部分复制的核心机制,本质上是一个参数化的数据库查询视图,包含:
- 要同步的表定义
- 过滤条件(WHERE子句)
- 列投影(SELECT子句)
- 排序与分页参数
从代码层面看,Shape类封装了数据订阅、更新通知和状态管理的完整生命周期:
// packages/typescript-client/src/shape.ts
export class Shape<T extends Row<unknown> = Row> {
readonly stream: ShapeStreamInterface<T>
readonly #data: ShapeData<T> = new Map() // 存储当前形状数据
readonly #subscribers = new Map<number, ShapeChangedCallback<T>>() // 订阅者列表
#status: ShapeStatus = `syncing` // 同步状态
// 处理流消息,更新本地数据
#process(messages: Message<T>[]): void {
let shouldNotify = false
messages.forEach((message) => {
if (isChangeMessage(message)) {
shouldNotify = this.#updateShapeStatus(`syncing`)
switch (message.headers.operation) {
case `insert`:
this.#data.set(message.key, message.value)
break
case `update`:
this.#data.set(message.key, {
...this.#data.get(message.key)!,
...message.value,
})
break
case `delete`:
this.#data.delete(message.key)
break
}
}
})
if (shouldNotify) this.#notify()
}
}
基于Shapes的租户隔离实现
1. 基础隔离模式:租户ID过滤
最常用的隔离方式是在Shape定义中加入租户ID过滤条件,确保每个租户只能访问自己的数据。Electric的代理认证示例展示了这一实现:
// examples/proxy-auth/app/shape-proxy/route.ts
export async function GET(request: Request) {
const url = new URL(request.url)
const originUrl = new URL(`/v1/shape`, process.env.ELECTRIC_URL)
// 从认证头获取租户ID
const org_id = request.headers.get(`authorization`)
// 关键:设置租户过滤条件
originUrl.searchParams.set(`where`, `"org_id" = ${org_id}`)
// 转发请求到Electric服务
return fetch(originUrl)
}
2. 高级隔离:JWT验证与形状签名
Gatekeeper认证示例展示了更严格的隔离策略,通过JWT签名形状定义,防止客户端篡改过滤条件:
// examples/gatekeeper-auth/edge/index.ts
function matchesDefinition(shape: ShapeDefinition, params: URLSearchParams) {
// 验证请求参数与JWT中的形状定义是否一致
if (shape.table !== params.get("table")) return false
if (shape.where !== params.get("where")) return false // 防止租户ID篡改
if (shape.columns !== params.get("columns")) return false
return true
}
Deno.serve((req) => {
const [isValidJWT, claims] = verifyAuthHeader(req.headers)
if (!isValidJWT) return new Response("Unauthorized", { status: 401 })
// 验证形状定义
if (!matchesDefinition(claims.shape, url.searchParams)) {
return new Response("Forbidden", { status: 403 })
}
return fetch(`${ELECTRIC_URL}/v1/shape${url.search}`)
})
3. 隔离实现流程图
共享数据策略:打破租户边界的数据协作
共享模式1:基于角色的访问控制
通过在表中添加角色权限列,实现跨租户数据访问的精细化控制。TanStack示例中展示了如何通过user_ids数组实现多用户共享:
// examples/tanstack-db-web-starter/src/routes/api/todos.ts
// 共享给指定用户的任务
const filter = `'${session.user.id}' = ANY(user_ids)`
originUrl.searchParams.set("where", filter)
数据库表结构设计:
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
user_ids TEXT[] NOT NULL DEFAULT '{}', -- 共享用户ID数组
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
共享模式2:公共数据多租户共享
对于产品目录、地区信息等公共数据,可通过命名空间隔离实现高效共享:
// 公共数据形状 (所有租户可见)
const publicShape = new ShapeStream({
url: "http://localhost:3000/v1/shape",
params: {
table: "public.product_catalog",
columns: "id,name,price"
}
})
// 租户私有数据形状
const tenantShape = new ShapeStream({
url: "http://localhost:3000/v1/shape",
params: {
table: "todos",
where: `org_id=${currentTenantId}`
}
})
共享模式3:跨租户协作区
通过中间关联表实现租户间数据共享,适用于需要多方协作的场景:
实现代码示例:
// 获取当前租户可访问的协作资源
const getCollaborativeResources = (tenantId) => {
return new ShapeStream({
url: "http://localhost:3000/v1/shape",
params: {
table: "resources",
where: `exists (
select 1 from collaborations
where collaborations.resource_id = resources.id
and collaborations.tenant_id = ${tenantId}
)`
}
})
}
实现案例:从0到1构建多租户应用
步骤1:数据库设计与初始化
-- 启用行级安全
ALTER DATABASE electric ENABLE ROW LEVEL SECURITY;
-- 创建租户表
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
domain TEXT UNIQUE,
settings JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建用户表
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建示例业务表(带租户ID)
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
description TEXT,
shared_tenant_ids UUID[] DEFAULT '{}', -- 共享租户ID数组
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- 创建行级安全策略
CREATE POLICY tenant_isolation_policy ON projects
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
步骤2:Electric服务配置
# docker-compose.yaml
version: '3.8'
services:
electric:
image: electricsql/electric:latest
environment:
DATABASE_URL: "postgresql://postgres:password@postgres:5432/electric"
ELECTRIC_PORT: 3000
LOGICAL_PUBLISHER_HOST: "postgres"
DB_POOL_SIZE: 10 # 调整连接池大小适应多租户负载
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:14
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: electric
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
步骤3:多租户Shape实现
// src/lib/electric.ts
import { ElectricClient, ShapeStream } from '@electric-sql/client'
// 初始化Electric客户端
const electric = new ElectricClient({
url: import.meta.env.VITE_ELECTRIC_URL || 'http://localhost:3000'
})
// 创建租户隔离形状工厂
export function createTenantShape<T>({
table,
columns = '*',
additionalWhere = 'true'
}) {
// 从认证存储获取当前租户ID
const tenantId = localStorage.getItem('tenant_id')
// 构建形状参数
const params = new URLSearchParams({
table,
columns,
where: `tenant_id = '${tenantId}' AND ${additionalWhere}`
})
return new ShapeStream<T>({
url: `${electric.baseUrl}/v1/shape`,
params
})
}
// 用法示例:获取当前租户的项目列表
export const projectsShape = createTenantShape<Project>({
table: 'projects',
columns: 'id,name,description,created_at'
})
// 共享项目形状
export const sharedProjectsShape = createTenantShape<Project>({
table: 'projects',
additionalWhere: `'${tenantId}' = ANY(shared_tenant_ids)`
})
步骤4:前端集成与状态管理
// src/components/ProjectsList.tsx
import { Shape } from '@electric-sql/client'
import { projectsShape } from '../lib/electric'
export function ProjectsList() {
const [projects, setProjects] = useState<Project[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const shape = new Shape(projectsShape)
// 订阅数据更新
const unsubscribe = shape.subscribe(({ rows }) => {
setProjects(rows)
setLoading(false)
})
// 初始加载数据
shape.rows.then(initialRows => {
setProjects(initialRows)
setLoading(false)
})
return () => {
unsubscribe()
shape.stream.close()
}
}, [])
if (loading) return <Spinner />
return (
<div className="space-y-4">
{projects.map(project => (
<ProjectCard key={project.id} project={project} />
))}
</div>
)
}
性能优化与安全加固
性能优化策略
1. 形状缓存与预计算
Electric的ShapeCache模块提供了形状结果缓存机制,可显著降低数据库负载:
# packages/sync-service/lib/electric/shapes.ex
def get_or_create_shape_handle(config, shape_def) do
{shape_cache, opts} = Access.get(config, :shape_cache, {ShapeCache, []})
shape_cache.get_or_create_shape_handle(
shape_def,
Keyword.put(opts, :otel_ctx, :otel_ctx.get_current())
)
end
缓存配置建议:
- 高频访问的公共数据:TTL 5-15分钟
- 租户私有数据:TTL 1-5分钟或禁用缓存
- 实时性要求高的数据:使用SSE流模式
2. 索引优化
为租户过滤条件创建合适的索引是性能优化的关键:
-- 租户ID单列索引
CREATE INDEX idx_projects_tenant_id ON projects(tenant_id);
-- 复合索引(租户ID+常用过滤条件)
CREATE INDEX idx_todos_tenant_status ON todos(tenant_id, completed);
-- 数组包含查询优化(用于共享数据)
CREATE INDEX idx_todos_user_ids ON todos USING GIN (user_ids);
3. 查询性能监控
通过Electric的日志系统监控慢查询:
# 配置慢查询日志阈值(毫秒)
config :electric,
slow_query_threshold: 200,
log_slow_queries: true
安全加固措施
1. 防御SQL注入
虽然Electric使用参数化查询,但仍需注意输入验证:
// 不安全:直接拼接SQL
const unsafeFilter = `user_id = ${userId}` // ❌
// 安全:使用参数化查询
const safeFilter = `user_id = '${sanitizeUUID(userId)}'` // ✅
// UUID sanitization函数
function sanitizeUUID(uuid: string): string {
if (!/^[0-9a-f-]{36}$/i.test(uuid)) {
throw new Error('Invalid UUID format')
}
return uuid
}
2. 敏感数据加密
对于租户敏感数据,可结合Postgres的pgcrypto扩展:
-- 创建加密列
ALTER TABLE tenants ADD COLUMN api_keys BYTEA;
-- 插入加密数据
INSERT INTO tenants (name, api_keys)
VALUES ('Acme Corp', pgp_sym_encrypt('{"stripe":"sk_test_xxx"}', :encryption_key));
-- 查询解密数据(在Electric形状中处理)
SELECT id, name, pgp_sym_decrypt(api_keys, :encryption_key) as api_keys
FROM tenants WHERE id = :tenant_id;
3. 审计日志
实现完整的数据访问审计机制:
CREATE TABLE data_access_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
shape_handle TEXT NOT NULL, -- 记录访问的形状
query_params JSONB NOT NULL,
accessed_at TIMESTAMP NOT NULL DEFAULT NOW(),
ip_address TEXT NOT NULL
);
高级应用模式
多租户SaaS的微服务架构
结合Electric的跨服务同步能力,构建松耦合的微服务架构:
租户数据生命周期管理
实现租户数据的完整生命周期管理,包括:
- 租户预配置:创建包含初始数据的租户模板
- 数据归档:对非活跃租户数据进行归档
- 数据清理:安全删除租户数据(级联删除)
// 租户清理函数
async function deleteTenant(tenantId: string): Promise<void> {
// 1. 验证权限
if (!isSystemAdmin()) throw new Error('Permission denied')
// 2. 标记租户为删除中
await api.patch(`/tenants/${tenantId}`, { status: 'deleting' })
// 3. 清理相关形状数据
await electricClient.cleanShapes({
where: `tenant_id = '${tenantId}'`
})
// 4. 执行级联删除
await api.delete(`/tenants/${tenantId}`)
// 5. 记录审计日志
await logAuditEvent({
action: 'tenant_deleted',
targetId: tenantId,
details: { timestamp: new Date().toISOString() }
})
}
总结与展望
Electric的Shapes技术为多租户架构带来了革命性的解决方案,通过逻辑隔离而非物理隔离的创新方式,解决了传统多租户方案中资源利用率与数据安全性之间的矛盾。本文详细介绍了基于Shapes的隔离与共享策略,从基础实现到高级应用,涵盖了多租户设计的各个方面。
关键要点回顾
- Shape隔离:通过where条件实现租户数据边界,结合JWT验证确保安全性
- 数据共享:三种模式满足不同场景需求:用户级共享、公共数据、协作区
- 性能优化:缓存策略、索引设计和查询优化的最佳实践
- 安全加固:输入验证、数据加密和审计日志的完整实现
后续学习路径
- 深入学习Electric的CDC(变更数据捕获)机制
- 探索多区域部署的租户数据策略
- 研究基于AI的租户行为分析与异常检测
行动指南
-
立即克隆仓库体验多租户示例:
git clone https://gitcode.com/GitHub_Trending/el/electric cd electric/examples/proxy-auth docker compose up -
参考本文实现的多租户架构,评估现有系统的改造可能性
-
关注Electric官方文档,获取最新的功能更新和最佳实践
通过Electric的多租户设计,你的SaaS应用将获得更强的扩展性、更高的资源利用率和更精细的安全控制,为业务增长提供坚实的技术基础。
点赞+收藏+关注,获取更多Electric高级应用实践。下一篇我们将深入探讨"基于LLM的租户数据分析与智能推荐系统",敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



