ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

BiSheng 多租户架构实战:基于 ContextVar 的逻辑隔离与 SQLAlchemy 自动租户过滤全解析

BiSheng 多租户架构实战:基于 ContextVar 的逻辑隔离与 SQLAlchemy 自动租户过滤全解析 BiSheng 多租户架构实战基于 ContextVar 的逻辑隔离与 SQLAlchemy 自动租户过滤全解析【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bishengBiSheng开源 LLM DevOps 平台自 v2.5.0 起引入了基于逻辑隔离的多租户架构在同一数据库实例内通过tenant_id列实现数据隔离覆盖 44 张业务表和 5 种存储引擎MySQL、Redis、Milvus、Elasticsearch、MinIO。本文以 docs/architecture/12-multi-tenant.md 为骨架结合src/backend/bisheng/下的真实源码逐层拆解租户上下文传播、SQLAlchemy 事件钩子自动过滤、HTTP/WebSocket/Celery 全链路隔离、存储前缀策略、登录选租户流程与开发注意事项帮助你理解并落地企业级多租户隔离能力。1. 架构总览设计原则逻辑隔离所有租户共享同一数据库实例通过tenant_id列区分数据。选择逻辑隔离而非物理隔离独立库/独立实例的原因降低运维复杂度、支持跨租户聚合查询、简化部署。向后兼容默认租户id1在所有外部存储中不添加前缀与 v2.4.x 的数据路径完全一致。透明接入业务代码无需手动添加WHERE tenant_idXSQLAlchemy 事件钩子自动注入。租户隔离全链路从浏览器请求到存储引擎的完整链路如下中间件从 JWT/ASGI Cookie 解析出tenant_id写入 ContextVarSQLAlchemy 事件钩子根据 ContextVar 对 SELECT 注入过滤、对 INSERT 自动填充存储层按租户加前缀异步任务则通过 Celery 信号在发布/执行前后传递并复位租户上下文。2. 配置与运行模式配置模型多租户配置定义在 src/backend/bisheng/core/config/multi_tenant.py# src/backend/bisheng/core/config/multi_tenant.py class MultiTenantConf(BaseModel): enabled: bool Field(defaultFalse) # 是否启用多租户 default_tenant_code: str Field(defaultdefault) # 默认租户编码该模型还包含 v2.5.1 F019 引入的admin_scope_ttl_seconds字段默认 14400 秒即 4 小时用于控制全局超级管理员租户管理视角admin scope在 Redis 中的 TTL每次命中管理 API 会滑动续期。config.yaml在服务配置中对应片段multi_tenant: enabled: false default_tenant_code: defaultenabledfalse是默认值也是从 v2.4.x 升级后的开箱状态——此时系统退化为单租户行为所有查询自动使用默认租户 id1无需任何上下文。两种运行模式维度单租户模式 (enabledfalse)多租户模式 (enabledtrue)默认行为所有查询自动使用DEFAULT_TENANT_ID1必须在请求上下文中设置tenant_id无上下文时回退到默认租户不报错抛出NoTenantContextError20004登录流程直接分配tenant_id1查询用户关联租户可能需要选择存储前缀永远为空原路径默认租户为空新租户加前缀适用场景v2.4.x 升级后的默认模式企业集团多组织部署这一“无上下文时是否抛错”的分支逻辑在 src/backend/bisheng/core/database/tenant_filter.py 的_resolve_tenant_id()中实现先取 ContextVar为空时检查settings.multi_tenant.enabled未启用则返回DEFAULT_TENANT_ID启用则抛出NoTenantContextError。3. 数据模型ER 关系核心模型为Tenant租户主表与UserTenant用户-租户关联表定义于 src/backend/bisheng/database/models/tenant.py从源码看v2.5.1 F011 为Tenant增加了租户树字段parent_tenant_idNULL 表示 Root 租户MVP 锁定两层模型、share_default_to_childrenRoot 创建的资源默认是否共享给子租户UserTenant则引入is_active字段1当前活跃叶子租户NULL历史记录唯一约束由uk_user_tenant(user_id, tenant_id)调整为uk_user_active(user_id, is_active)利用 MySQL UNIQUE 允许多个 NULL 的特性保证每个用户至多一个活跃叶子租户的同时保留历史行。租户感知的业务表44 张以下表均包含tenant_id列INT NOT NULL DEFAULT 1由 Alembic 迁移v2_5_0_f001_multi_tenant.py统一添加模块表名核心应用flow,flowversion,assistant,assistantlink,template标签/分组tag,taglink,group,groupresource,usergroup角色/权限role,roleaccess,userrole会话/消息chatmessage,message_session,t_report,t_variable_value知识库knowledge,knowledgefile,qaknowledge工具t_gpts_tools,t_gpts_tools_type渠道channel,channel_info_source,channel_article_read分享share_link消息收件箱inbox_message,inbox_message_read微调/部署finetune,presettrain,modeldeploy,server,sftmodelLinsightlinsight_sop,linsight_sop_record,linsight_session_version,linsight_execute_taskLLMllm_server,llm_model评测/标注evaluation,dataset,marktask,markrecord,markappuser审计/邀请auditlog,invitecode不包含tenant_id的表user用户全局、user_tenant关联表tenant_id 为 FK、tenant租户主表、config系统配置、recallchunk检索中间表、failed_tuple补偿表。值得注意的是tenant_filter.py中维护了一份_TENANT_AWARE_MODEL_MODULES显式导入清单其中注释明确记录了历史上knowledgefile / flowversion / roleaccess / userrole等模型曾因不在 FastAPI 路由导入链上而“静默逃逸”租户过滤、把子租户资源写进 Root 的线上事故。因此register_tenant_filter_events()会先通过_force_import_all_models()强制导入全部租户感知 ORM 模块再扫描 metadata从根上避免“导入不到 → 发现不了 → 不参与过滤”的漏网之鱼。4. 租户上下文传播ContextVar 机制租户隔离基于 Python 的contextvars模块天然支持线程安全和异步安全。核心定义在 src/backend/bisheng/core/context/tenant.py# src/backend/bisheng/core/context/tenant.py DEFAULT_TENANT_ID: int 1 # 请求作用域内的租户 ID current_tenant_id: ContextVar[Optional[int]] ContextVar(current_tenant_id, defaultNone) # 是否绕过租户过滤系统管理员跨租户查询 _bypass_tenant_filter: ContextVar[bool] ContextVar(_bypass_tenant_filter, defaultFalse)API 函数函数用途get_current_tenant_id()获取当前上下文的租户 ID未设置返回Noneset_current_tenant_id(tid)设置当前上下文的租户 IDbypass_tenant_filter()上下文管理器临时禁用租户过滤is_tenant_filter_bypassed()检查当前是否已绕过过滤v2.5.1 F012 在此基础上扩展了visible_tenant_ids当前请求可“看见”的租户 ID 集合用于 IN-list 过滤、strict_tenant_filter()强制严格等值过滤的上下文管理器供 F016 配额统计等 IN-list 会超算的场景、以及 F019 的_admin_scope_tenant_id全局超管的管理视角覆盖设置后get_current_tenant_id()优先返回该值。上下文设置时机场景设置方文件HTTP 请求CustomMiddleware从 JWT Cookie 解码src/backend/bisheng/utils/http_middleware.pyWebSocketWebSocketLoggingMiddleware从 ASGI Cookie 解码src/backend/bisheng/utils/http_middleware.pyCelery 任务task_prerun信号从 headers 恢复src/backend/bisheng/worker/tenant_context.py系统初始化显式调用set_current_tenant_id()或bypass_tenant_filter()src/backend/bisheng/common/init_data.py5. 自动租户过滤SQLAlchemy 事件钩子核心文件src/backend/bisheng/core/database/tenant_filter.py注册时机src/backend/bisheng/core/database/manager.py 中的DatabaseManager._register_tenant_filter()约第 59-63 行在数据库连接管理器初始化时调用register_tenant_filter_events()注册全局 Session 事件。该函数幂等_initialized标志保证多次调用只注册一次。自动发现机制_discover_tenant_aware_tables()扫描 SQLModel 的 metadata自动发现所有包含tenant_id列的表。新增 ORM 模型只要声明了tenant_id字段即可自动参与过滤无需额外注册。排除列表_EXCLUDED_TABLES {user_tenant}user_tenant的tenant_id是外键关联字段非隔离字段。SELECT 拦截流程do_orm_execute事件拦截 ORM SELECT 并注入过滤条件_get_tenant_tables_from_statement()通过两种方式提取查询中的表column_descriptions— 适用于select(Model)模式get_final_froms/froms回退 — 适用于 joins 和 subquery。当设置了visible_tenant_idsF012时事件监听器会注入tenant_id IN (...)列表过滤而非单值等值过滤对全局超管visible_tenant_idsNone则不注入过滤。INSERT 自动填充before_flush事件在session.new中遍历待插入对象若对象所属表在_tenant_aware_tables中且tenant_id为None或0自动填充为当前上下文的租户 ID多租户模式下若无上下文跳过填充由后续 SELECT 时的_resolve_tenant_id()捕获异常源码还实现了纵深防御当对象显式携带的tenant_id与上下文不一致时写入按显式值继续但记录tenant_id mismatch on write告警日志便于灰度期暴露异常写入。此外build_tenant_filter_clause()提供与事件监听器完全一致的过滤语义含 bypass / visible-ids / strict 分支供select(sub.c.id) FROM (SELECT ... FROM flow UNION ALL ...)这类内层表被 Subquery 隐藏、事件监听器无法发现租户感知表的 SQL 形态下手动拼接过滤条件保证手动路径与事件路径不脱节。已知限制text()构造的原生 SQL不触发ORM 事件。使用原生 SQL 时必须手动添加WHERE tenant_id X。6. HTTP / WebSocket 中间件核心文件src/backend/bisheng/utils/http_middleware.py请求处理流程源码层面CustomMiddleware.dispatch()的具体逻辑先通过_extract_http_access_token()解析 JWT——优先 HttpOnly Cookieaccess_token_cookie其次支持Authorization: Bearerplatform SPA 走 localStorage Bearer随后_set_tenant_context()解码并写入 ContextVartoken 缺失且多租户未启用时回退默认租户非豁免路径还会执行 F012 的token_version校验Redis 缓存 5 分钟基础设施故障时 fail-open与visible_tenant_ids计算并依据DISABLED_TENANT_KEYdisabled_tenant:{id}Redis 黑名单拦截已禁用租户403 20001。豁免路径整体在_bypass_tenant_filter.set(True)下运行避免登录等前置流程触碰租户感知表时触发NoTenantContextError。豁免路径以下路径不进行租户状态检查登录前或系统级接口/api/v1/user/login /api/v1/user/regist /api/v1/user/sso # legacy compatibility only; new third-party login uses /api/v1/internal/sso/login-sync /api/v1/user/ldap /api/v1/user/public_key /api/v1/user/get_captcha /api/v1/user/switch-tenant /api/v1/user/tenants /api/v1/internal/sso/login-sync /api/v1/internal/sso/gateway-wecom-org-sync /api/v1/departments/sync /api/v1/internal/departments/relink /api/v1/internal/departments/relink/resolve-conflict /api/v1/share-link /api/v1/env /health /docs /openapi.json /redoc其中 v2.5.1 新增的内部路径HMAC 认证的 Gateway 回调、部门同步/重连、匿名分享链接读取无 JWT由服务层显式安装ROOT_TENANT_IDbypass_tenant_filter()。WebSocket 中间件WebSocketLoggingMiddleware从 ASGI scope 的 headers 中解析 Cookie_get_cookie_from_scope调用与 HTTP 相同的_set_tenant_context()函数设置租户上下文保证长连接链路同样享受隔离。7. 存储隔离策略核心文件src/backend/bisheng/core/storage/tenant_storage.py前缀规则默认租户id1不添加前缀保持与 v2.4.x 完全兼容。新租户使用各存储引擎对应的前缀存储引擎默认租户 (id1)新租户 (id2, codeacme)函数MinIO(原路径)tenant_acme/get_minio_prefix(tenant_id, tenant_code)Milvus(原集合名)t2_get_milvus_collection_prefix(tenant_id)Elasticsearch(原索引名)t2_get_es_index_prefix(tenant_id)Redis(原 key)t:2:get_redis_key_prefix(tenant_id)从源码可见四个函数都遵循同一模式if tenant_id DEFAULT_TENANT_ID: return 其余按各自规则拼前缀MinIO 用tenant_{code}/Milvus/ES 用t{id}_Redis 用t:{id}:。文件头注释明确这些函数只定义前缀约定实际存储调用点在各业务模块拼接前缀后访问存储引擎F008-resource-rebac-adaptation 中修改。使用方式MinIO文件上传路径拼接{prefix}{original_path}Milvus知识库创建时 collection 名为{prefix}{collection_name}ES索引创建时名为{prefix}{index_name}Redis缓存 key 拼接{prefix}{original_key}8. Celery 任务租户上下文传递核心文件src/backend/bisheng/worker/tenant_context.py信号流程三个 Celery 信号信号时机作用before_task_publishAPI 进程发布任务时将当前tenant_id写入任务headerstask_prerunWorker 执行任务前从headers恢复ContextVar无值时回退到DEFAULT_TENANT_IDtask_postrunWorker 执行任务后重置ContextVar为None防止线程池复用时泄露信号注册方式src/backend/bisheng/worker/main.py 中import bisheng.worker.tenant_context触发模块级信号绑定。源码中restore_tenant_context通过sender.request.headers读取tenant_id并int()转换后写入reset_tenant_context则直接current_tenant_id.set(None)。9. 登录与租户选择流程完整流程JWT Payload 结构{ user_id: 1, user_name: admin, tenant_id: 2 }tenant_id0是一个临时状态表示用户已认证但尚未选择租户。中间件对此状态的非豁免路径返回 403status_code: 20004, status_message: Missing tenant context。v2.5.1 变更提示从当前仓库 src/backend/bisheng/tenant/api/endpoints/user_tenant.py 看POST /api/v1/user/switch-tenant已标注为 DEPRECATEDv2.5.1 F011 起租户切换移除用户所属租户改由 F012 TenantResolver 依据主部门自动解析不再存在用户可见的“切换”动作GET /api/v1/user/tenants仍保留用于获取我的租户列表。该端点仍保留在豁免路径中仅为兼容旧客户端。10. 租户管理 API端点清单管理员端点需要系统管理员权限实现在 src/backend/bisheng/tenant/api/endpoints/tenant_crud.py端点方法说明/api/v1/tenants/POST创建租户含根部门 UserTenant OpenFGA 元组/api/v1/tenants/GET租户列表分页 关键词搜索 状态筛选/api/v1/tenants/{id}GET租户详情含管理员用户列表/api/v1/tenants/{id}PUT更新租户信息名称 / logo / 联系人/api/v1/tenants/{id}DELETE删除租户须无活跃用户/api/v1/tenants/{id}/statusPUT状态管理active / disabled / archived/api/v1/tenants/{id}/quotaGET/PUT配额查看 / 设置/api/v1/tenants/{id}/usersGET/POST/DELETE租户用户管理用户端点已登录用户实现在 src/backend/bisheng/tenant/api/endpoints/user_tenant.py端点方法说明/api/v1/user/tenantsGET获取我的可用租户列表/api/v1/user/switch-tenantPOST切换租户签发新 JWTv2.5.1 起 DEPRECATED业务逻辑集中在 src/backend/bisheng/tenant/domain/services/tenant_service.py 的TenantServiceDAO 层TenantDao/UserTenantDaosrc/backend/bisheng/database/models/tenant.py提供了跨租户列表、租户用户分页、批量tenant_id改写F011 卸载/迁移、活跃叶子租户解析等能力——跨租户的 DAO 方法普遍以with bypass_tenant_filter():包裹。保护规则默认租户id1不可删除不可修改tenant_code删除租户前须确认无活跃用户TenantHasUsersError禁用租户时向 Redis 写入黑名单 keydisabled_tenant:{id}中间件实时生效不能移除租户最后一个管理员TenantAdminRequiredError从TenantDao.alist_tenants()源码可见列表接口默认排除archived状态终态仅供审计需要时可通过显式传statusarchived查询。创建租户的原子流程1. 创建 Tenant 记录检查 tenant_code 唯一性 2. 创建根部门DepartmentService.acreate_root_department 3. 回写 Tenant.root_dept_id 4. 为管理员用户创建 UserTenant 记录 5. 写入 OpenFGA 权限元组admin member11. 数据库迁移与初始化Alembic 迁移迁移文件src/backend/bisheng/core/database/alembic/versions/v2_5_0_f001_multi_tenant.pyupgrade 流程创建tenant表创建user_tenant表含uk_user_tenant唯一约束为 44 张业务表添加tenant_id列INT NOT NULL DEFAULT 1 索引种子数据插入默认租户 (id1, codedefault, nameDefault Tenant)回填为所有现有用户创建user_tenant记录关联默认租户downgrade逆序删除tenant_id列和表。注意 tenant_id 1 的数据上下文会丢失。应用初始化src/backend/bisheng/common/init_data.py 中的相关函数函数Feature作用_init_default_tenant()F001确保默认租户存在回填user_tenant_init_default_root_department()F002为默认租户创建根部门_migrate_rbac_to_rebac_if_needed()F006一次性 RBAC → ReBAC 迁移Redis SETNX 锁保证幂等这些函数在应用启动时lifespan调用使用bypass_tenant_filter()绕过租户过滤——因为启动阶段请求上下文尚未建立。12. 前端集成租户选择页src/frontend/platform/src/pages/LoginPage/TenantSelect.tsx在登录后若用户有多个可用租户前端跳转到租户选择页使用sessionStorage缓存待选租户列表来自登录 API 响应用户点击租户后调用switchTenantApi(tenantId)获取新 JWT成功后跳转到应用首页租户管理页src/frontend/platform/src/pages/TenantPage/系统管理员可见的租户管理面板租户列表表格 搜索 状态筛选子组件CreateTenantDialog创建/编辑、TenantUserDialog用户管理、TenantQuotaDialog配额设置删除租户需输入tenant_code二次确认API 层src/frontend/platform/src/controllers/API/tenant.ts 封装了所有租户相关 API 调用。13. 错误码模块编码2005 位编码200XX定义于 src/backend/bisheng/common/errcode/tenant.py错误码类名说明20000TenantNotFoundError租户不存在20001TenantDisabledError租户已禁用20002UserNotInTenantError用户不属于该租户20003TenantCodeDuplicateError租户编码重复20004NoTenantContextError多租户模式下缺少租户上下文20005TenantHasUsersError删除租户时仍有活跃用户20006TenantAdminRequiredError不能移除租户最后一个管理员20007TenantSwitchForbiddenError用户不属于目标租户20008TenantCreationFailedError租户创建失败20009NoTenantsAvailableError用户无可用租户14. 开发者注意事项原生 SQL 的租户过滤text()构造的原生 SQL 绕过 ORM 事件不会自动注入WHERE tenant_idX。使用原生 SQL 时必须手动添加租户过滤# 错误 — 无租户过滤 session.execute(text(SELECT * FROM flow WHERE status active)) # 正确 — 手动添加 tenant_id tid get_current_tenant_id() session.execute(text(SELECT * FROM flow WHERE status active AND tenant_id :tid), {tid: tid})bypass_tenant_filter 使用场景bypass_tenant_filter()是一个上下文管理器临时禁用 SELECT 过滤和 INSERT 自动填充。仅在以下场景使用系统初始化init_data.py系统管理员跨租户管理查询TenantDao内部登录/注册流程用户尚无租户上下文数据迁移脚本from bisheng.core.context.tenant import bypass_tenant_filter with bypass_tenant_filter(): # 此上下文内的查询不带 WHERE tenant_idX all_tenants TenantDao.get_all() # 退出后自动恢复过滤新增 ORM 模型只要在模型中声明tenant_id列_discover_tenant_aware_tables()会在应用启动时自动发现无需额外注册class MyNewModel(SQLModel, tableTrue): id: int Field(primary_keyTrue) tenant_id: int Field(default0, indexTrue) # 声明即参与自动过滤 name: str不过需要注意register_tenant_filter_events()的发现机制依赖 metadata 中已注册的模型若新模型不在任何导入链上需要同步加入_TENANT_AWARE_MODEL_MODULES强制导入清单该清单的注释中记录了历史上因此导致租户数据泄漏的真实事故务必引以为戒。Celery 任务任务函数内的 ORM 操作自动享受租户隔离通过task_prerun信号恢复上下文。但若通过其他方式直接操作存储如 RedisSET/GET、MinIO 上传需手动拼接前缀。测试测试代码中需显式设置租户上下文from bisheng.core.context.tenant import set_current_tenant_id, bypass_tenant_filter def test_something(): set_current_tenant_id(1) # ... 测试代码 def test_cross_tenant(): with bypass_tenant_filter(): # ... 跨租户查询默认租户保护id1 的默认租户具有特殊保护不可删除、不可修改状态、存储前缀为空。对默认租户的操作应格外谨慎。15. 关键源码索引功能文件路径关键内容租户上下文 ContextVarsrc/backend/bisheng/core/context/tenant.pycurrent_tenant_id,bypass_tenant_filter()SQLAlchemy 自动过滤src/backend/bisheng/core/database/tenant_filter.pydo_orm_execute,before_flush事件钩子过滤事件注册src/backend/bisheng/core/database/manager.pyDatabaseManager._register_tenant_filter()约第 59-63 行多租户配置src/backend/bisheng/core/config/multi_tenant.pyMultiTenantConf存储隔离前缀src/backend/bisheng/core/storage/tenant_storage.py4 种前缀函数HTTP 中间件src/backend/bisheng/utils/http_middleware.pyCustomMiddleware,WebSocketLoggingMiddlewareCelery 信号src/backend/bisheng/worker/tenant_context.py3 个 Celery signalsWorker 信号注册src/backend/bisheng/worker/main.pyimport bisheng.worker.tenant_contextTenant ORM 模型src/backend/bisheng/database/models/tenant.pyTenant,UserTenant, DAO 类租户管理服务src/backend/bisheng/tenant/domain/services/tenant_service.pyTenantService业务逻辑租户 CRUD APIsrc/backend/bisheng/tenant/api/endpoints/tenant_crud.py管理员 CRUD 端点用户租户 APIsrc/backend/bisheng/tenant/api/endpoints/user_tenant.pytenants列表switch-tenant 已废弃错误码src/backend/bisheng/common/errcode/tenant.py20000-20009Alembic 迁移src/backend/bisheng/core/database/alembic/versions/v2_5_0_f001_multi_tenant.py表创建 44 表加列初始化数据src/backend/bisheng/common/init_data.py_init_default_tenant(),_init_default_root_department()前端租户选择src/frontend/platform/src/pages/LoginPage/TenantSelect.tsx租户选择页面前端租户 APIsrc/frontend/platform/src/controllers/API/tenant.tsAPI 调用封装前端租户管理src/frontend/platform/src/pages/TenantPage/管理页面相关文档系统架构总览 — 运行时组件和数据流全景数据模型与存储层 — ORM 模型完整清单部署架构与配置 — 配置系统和环境变量用户与权限体系 — RBAC/ReBAC 权限模型商业版 API 网关 — Gateway 与多租户的集成方向【免费下载链接】bishengBISHENG is an open LLM devops platform for next generation Enterprise AI applications. Powerful and comprehensive features include: GenAI workflow, RAG, Agent, Unified model management, Evaluation, SFT, Dataset Management, Enterprise-level System Management, Observability and more.项目地址: https://gitcode.com/GitHub_Trending/bi/bisheng创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表