多租户文档

概述

系统基于字段隔离的多租户架构设计,使用tenant_id作为租户标识符,实现在单一数据库实例中为多个租户提供隔离的数据存储和服务。

基于字段隔离的多租户架构是一种成本效益高的解决方案,适用于中小型应用。通过合理的设计和实现,可以在保证数据隔离的同时,实现资源的共享和高效利用。关键是要在应用层的各个层面都严格实施租户隔离策略,确保数据安全和系统稳定性。

架构模式

字段隔离(共享数据库,共享表结构)

字段隔离是一种多租户架构模式,所有租户共享同一个数据库实例和表结构,通过在每个表中添加tenant_id字段来区分不同租户的数据。

+-----------------+     +-----------------+     +-----------------+
|   租户 A 数据    |     |   租户 B 数据    |     |   租户 C 数据    |
|   tenant_id=1   |     |   tenant_id=2   |     |   tenant_id=3   |
+-----------------+     +-----------------+     +-----------------+
\                 /     \                 /     \                 /
 \               /       \               /       \               /
  +-------------------------------------------------------------+
  |                    共享数据库表结构                         |
  |          (所有数据存储在同一表中,通过tenant_id区分)         |
  +-------------------------------------------------------------+

安装部署方式

因为使用的是数据表字段隔离的方式,所以安装部署方式与相关配置和授权版操作完全一致。

数据库设计

基础表结构

所有业务表都需要包含tenant_id字段作为租户标识:

-- 用户表示例
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    tenant_id BIGINT NOT NULL,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_tenant_id (tenant_id),
    UNIQUE KEY uk_tenant_username (tenant_id, username),
    UNIQUE KEY uk_tenant_email (tenant_id, email)
);

-- 订单表示例
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    tenant_id BIGINT NOT NULL,
    order_no VARCHAR(32) NOT NULL,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_tenant_id (tenant_id),
    INDEX idx_tenant_user (tenant_id, user_id),
    UNIQUE KEY uk_tenant_order_no (tenant_id, order_no)
);

租户表

-- 租户信息表
CREATE TABLE tenants (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    code VARCHAR(50) NOT NULL UNIQUE, #域名或租户代码
    status TINYINT NOT NULL DEFAULT 1,
    settings JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

应用层实现

1. 中间件处理租户ID

在路由文件中,分组路由加上service.Middleware().Tenant中间件处理

group.Middleware(service.Middleware().Ctx, service.Middleware().Tenant)

2. 服务层实现

logic文件中结构体包含filter.TenantFilter结构体

type sDemoGenTenant struct {
   filter.TenantFilter
}

查询数据:

func (s *sDemoGenTenant) List(ctx context.Context, req *model.DemoGenTenantSearchReq) (listRes *model.DemoGenTenantSearchRes, err error) {
    ...
    m := s.Model(ctx, dao.DemoGenTenant.Ctx(ctx), s.WithWhere())
    ...
    err = m.Page(req.PageNum, req.PageSize).Order(order).Scan(&res)
    ...
}

若需要表别名处理,添加s.WithAlias("别名")参数:

func (s *sDemoGenTenant) List(ctx context.Context, req *model.DemoGenTenantSearchReq) (listRes *model.DemoGenTenantSearchRes, err error) {
    ...
    m := s.Model(ctx, dao.DemoGenTenant.Ctx(ctx), s.WithWhere(),s.WithAlias("demo"))
    ...
    err = m.Page(req.PageNum, req.PageSize).Order(order).Scan(&res)
    ...
}

新增数据:

func (s *sDemoGenTenant) Add(ctx context.Context, req *model.DemoGenTenantAddReq) (err error) {
   ...
   s.Model(ctx, dao.DemoGenTenant.Ctx(ctx), s.WithHook()).
   Insert(data)
   //注意:data中不需要处理tenant_id字段,hook中自动处理
   ...
}

修改数据:

func (s *sDemoGenTenant) Edit(ctx context.Context, req *model.DemoGenTenantEditReq) (err error) {
    ...
    s.Model(ctx, dao.DemoGenTenant.Ctx(ctx), s.WithWhere()).
    WherePri(req.Id).Update(data)
    ...
}

删除数据:

func (s *sDemoGenTenant) Delete(ctx context.Context, ids []uint) (err error) {
    ...
    _,err=s.Model(ctx, dao.DemoGenTenant.Ctx(ctx), s.WithWhere()).
    Delete(dao.DemoGenTenant.Columns().Id+" in (?)", ids)
    ...
}

代码生成支持

代码生成支持多租户功能,只需要在表中存在tenant_id字段,便会自动生成租户处理功能。

安全考虑

1. 数据隔离

  • 所有数据库查询,修改,删除必须包含tenant_id条件 WithWhere方法自动添加过滤条件
  • 新增数据写入tenant_id ,WithHook自动处理
  • 使用中间件自动注入租户上下文
  • 在数据访问层强制验证租户权限

性能优化

1. 数据库索引

-- 为所有表的tenant_id字段创建索引
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);

-- 创建复合索引优化查询
CREATE INDEX idx_users_tenant_username ON users(tenant_id, username);
CREATE INDEX idx_orders_tenant_user ON orders(tenant_id, user_id);

3. 缓存策略

租户级别的缓存

20251016171627

使用租户ID作为缓存键的前缀

20251016171709

系统使用

不同租户下的用户只能访问自己归属的租户相关数据,不能跨租户访问数据(租户管理员除外),租户管理员可以选择登录不同租户来设置租户相关数据(比如设置不同租户的字典,参数等相关信息)

字典和参数分为全局和租户,全局:所有租户可以使用;租户:不同租户的字典和参数相互隔离,示例如下:

如:租户A只能使用全局和租户A的字典:

20251017102005

租户B只能使用全局和租户B的字典:

20251017102102

租户管理员在角色授权时,选择是租户管理员则可以在不同租户中切换维护租户数据:

20251017102345

注意:普通管理人员不要给租户维护相关的权限(这个权限应该只是给系统维护人员或运维人员)

作者:管理员  创建时间:2025-10-17 15:35
最后编辑:管理员  更新时间:2025-10-17 15:36