Electric多租户设计:Shapes隔离与共享数据策略

Electric多租户设计:Shapes隔离与共享数据策略

【免费下载链接】electric electric-sql/electric: 这是一个用于查询数据库的JavaScript库,支持多种数据库。适合用于需要使用JavaScript查询数据库的场景。特点:易于使用,支持多种数据库,具有灵活的查询构建和结果处理功能。 【免费下载链接】electric 项目地址: https://gitcode.com/GitHub_Trending/el/electric

引言:多租户架构的隐形挑战

你是否还在为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. 隔离实现流程图

mermaid

共享数据策略:打破租户边界的数据协作

共享模式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:跨租户协作区

通过中间关联表实现租户间数据共享,适用于需要多方协作的场景:

mermaid

实现代码示例:

// 获取当前租户可访问的协作资源
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的跨服务同步能力,构建松耦合的微服务架构:

mermaid

租户数据生命周期管理

实现租户数据的完整生命周期管理,包括:

  1. 租户预配置:创建包含初始数据的租户模板
  2. 数据归档:对非活跃租户数据进行归档
  3. 数据清理:安全删除租户数据(级联删除)
// 租户清理函数
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的隔离与共享策略,从基础实现到高级应用,涵盖了多租户设计的各个方面。

关键要点回顾

  1. Shape隔离:通过where条件实现租户数据边界,结合JWT验证确保安全性
  2. 数据共享:三种模式满足不同场景需求:用户级共享、公共数据、协作区
  3. 性能优化:缓存策略、索引设计和查询优化的最佳实践
  4. 安全加固:输入验证、数据加密和审计日志的完整实现

后续学习路径

  • 深入学习Electric的CDC(变更数据捕获)机制
  • 探索多区域部署的租户数据策略
  • 研究基于AI的租户行为分析与异常检测

行动指南

  1. 立即克隆仓库体验多租户示例:

    git clone https://gitcode.com/GitHub_Trending/el/electric
    cd electric/examples/proxy-auth
    docker compose up
    
  2. 参考本文实现的多租户架构,评估现有系统的改造可能性

  3. 关注Electric官方文档,获取最新的功能更新和最佳实践

通过Electric的多租户设计,你的SaaS应用将获得更强的扩展性、更高的资源利用率和更精细的安全控制,为业务增长提供坚实的技术基础。


点赞+收藏+关注,获取更多Electric高级应用实践。下一篇我们将深入探讨"基于LLM的租户数据分析与智能推荐系统",敬请期待!

【免费下载链接】electric electric-sql/electric: 这是一个用于查询数据库的JavaScript库,支持多种数据库。适合用于需要使用JavaScript查询数据库的场景。特点:易于使用,支持多种数据库,具有灵活的查询构建和结果处理功能。 【免费下载链接】electric 项目地址: https://gitcode.com/GitHub_Trending/el/electric

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值