tenant/

directory
v1.1.47 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 6, 2026 License: Apache-2.0

README

多租户配置管理模块

[Go Report Card] [Coverage]

为所有 JXT 微服务提供共享的租户配置管理,集成 ETCD、自动重连和持久化缓存降级。

目录


快速开始

安装
go get github.com/ChenBigdata421/jxt-core/sdk/pkg/tenant
基本用法
package main

import (
    "context"
    "log"

    "github.com/ChenBigdata421/jxt-core/sdk/pkg/tenant/provider"
    "github.com/ChenBigdata421/jxt-core/sdk/pkg/tenant/database"
    clientv3 "go.etcd.io/etcd/client/v3"
)

func main() {
    // 1. 创建 ETCD 客户端
    etcdClient, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer etcdClient.Close()

    // 2. 创建 Provider(带重试和缓存)
    prov, err := provider.NewProviderWithRetry(etcdClient,
        provider.WithNamespace("jxt/"),
        provider.WithConfigTypes(
            provider.ConfigTypeDatabase,
            provider.ConfigTypeFtp,
            provider.ConfigTypeStorage,
        ),
        provider.WithCache(provider.NewFileCache()),
    )
    if err != nil {
        log.Fatal(err)
    }

    // 3. 启动 Watch(带重试)
    ctx := context.Background()
    if err := prov.StartWatchWithRetry(ctx); err != nil {
        log.Fatal(err)
    }
    defer prov.StopWatch()

    // 4. 使用缓存
    dbCache := database.NewCache(prov)
    cfg, err := dbCache.GetByID(ctx, 1)
    if err != nil {
        log.Fatal(err)
    }

    // 使用配置...
    _ = cfg
}

核心概念

配置与状态分离

租户模块遵循 CQRS 原则,实现清晰的配置与状态分离:

┌─────────────────────────────────────────────────┐
│  ETCD(由 tenant-service 管理)                   │
│  - 数据库配置                                     │
│  - FTP 配置                                      │
│  - 存储配置                                       │
│  - 元数据: code, name, status                    │
└─────────────────────────────────────────────────┘
                    ↓ Watch
┌─────────────────────────────────────────────────┐
│  jxt-core Provider(内存缓存)                   │
│  - Copy-on-Write 无锁读取                        │
│  - 指数退避自动重连                               │
│  - 持久化文件缓存降级                             │
└─────────────────────────────────────────────────┘
                    ↓ Cache API
┌─────────────────────────────────────────────────┐
│  微服务(file-storage, evidence 等)              │
│  - database.Cache → DatabaseConfig               │
│  - ftp.Cache → FtpConfig                         │
│  - storage.Cache → StorageConfig                 │
└─────────────────────────────────────────────────┘
租户数据流
  1. 配置写入:tenant-service → ETCD
  2. 配置分发:ETCD Watch → Provider → 微服务
  3. 本地降级:ETCD 不可用 → 本地缓存文件 → 内存

架构设计

分层架构
┌─────────────────────────────────────────────────────┐
│  中间件层                                            │
│  - ExtractTenantID(4 种解析方式)                   │
│    * host:   子域名提取                              │
│    * header: 自定义 Header(默认: X-Tenant-ID)       │
│    * query:  查询参数(默认: tenant)                 │
│    * path:   URL 路径片段索引                        │
└─────────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────┐
│  缓存层(按需加载)                                   │
│  - database.Cache  → DatabaseConfig                 │
│  - ftp.Cache       → FtpConfig                      │
│  - storage.Cache   → StorageConfig                  │
└─────────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────┐
│  Provider 层(核心)                                  │
│  - ETCD Watch + 指数退避重连                         │
│  - Copy-on-Write 无锁读取                            │
│  - 持久化文件缓存降级                                 │
│  - 初始化重试机制                                     │
└─────────────────────────────────────────────────────┘
                         ↓
┌─────────────────────────────────────────────────────┐
│  持久化层                                            │
│  - ETCD(配置源)                                     │
│  - FileCache(本地降级)                              │
└─────────────────────────────────────────────────────┘
并发安全性
操作 机制 延迟
读取配置 atomic.Value ~25ns
更新配置 Copy-on-Write ~1μs
Watch 重连 指数退避 1s→30s -

API 参考

Provider 配置选项
// 命名空间配置
WithNamespace(ns string) Option

// 配置类型过滤(按需加载)
WithConfigTypes(types ...ConfigType) Option

// 本地缓存降级
WithCache(cache FileCache) Option
中间件配置选项
// ========== 统一配置(推荐) ==========

// 从 Provider 自动读取全部配置(httpType、httpHostMode 等)
// 这是推荐的方式,所有配置由 ETCD 统一管理
WithProviderConfig(provider ProviderConfigurer) Option

// ========== 解析类型配置 ==========

// 设置解析类型:host/header/query/path
WithResolverType(resolverType string) Option

// Header 模式:设置 Header 名称(默认: X-Tenant-ID)
WithHeaderName(name string) Option

// Query 模式:设置查询参数名(默认: tenant)
WithQueryParam(param string) Option

// Path 模式:设置路径索引(默认: 0)
WithPathIndex(index int) Option

// ========== 行为配置 ==========

// 设置租户缺失时的行为
// - "Abort"(默认):返回 400 错误,中断请求
// - "Continue":继续执行,租户 ID 为 0
WithOnMissingTenant(mode string) Option
核心方法
方法 描述 返回值
NewProvider(client, opts...) 创建基础 Provider *Provider
NewProviderWithRetry(client, opts...) 创建并初始化(带重试) *Provider, error
LoadAll(ctx) 从 ETCD 加载所有配置 error
StartWatch(ctx) 启动 ETCD 监听 error
StartWatchWithRetry(ctx) 启动监听(带重试) error
StopWatch() 停止监听 -
GetDatabaseConfig(id) 获取数据库配置 *DatabaseConfig, bool
GetFtpConfig(id) 获取 FTP 配置 *FtpConfig, bool
GetStorageConfig(id) 获取存储配置 *StorageConfig, bool
GetTenantMeta(id) 获取租户元数据 *TenantMeta, bool
IsTenantEnabled(id) 检查租户是否激活 bool
NewFileCache() 创建默认文件缓存 FileCache
NewFileCacheWithPath(path) 创建指定路径的文件缓存 FileCache
GetResolverConfig() 获取租户识别配置 *ResolverConfig
GetTenantIDByDomain(domain) 通过域名查找租户 ID int, bool
GetTenantIDByCode(code) 通过租户代码查找租户 ID int, bool
中间件辅助函数
函数 描述 返回值
GetTenantID(c) 从 Gin Context 获取租户 ID int(未找到返回 0)
GetTenantIDAsInt(c) 从 Gin Context 获取租户 ID(带错误) int, error
MustGetTenantID(c) 获取租户 ID,不存在则 panic int
Cache API
database.Cache
func NewCache(prov *provider.Provider) *Cache
func (c *Cache) GetByID(ctx context.Context, tenantID int) (*TenantDatabaseConfig, error)
// 注意: GetByCode(ctx, code string) 计划在未来版本中实现
ftp.Cache
func NewCache(prov *provider.Provider) *Cache
func (c *Cache) GetByID(ctx context.Context, tenantID int) (*TenantFtpConfig, error)
storage.Cache
func NewCache(prov *provider.Provider) *Cache
func (c *Cache) GetByID(ctx context.Context, tenantID int) (*TenantStorageConfig, error)

使用示例

场景 1:数据库连接
dbCache := database.NewCache(prov)

cfg, err := dbCache.GetByID(ctx, tenantID)
if err != nil {
    return err
}

// 创建 GORM 数据库连接
gormDB, err := gorm.Open(mysql.Open(fmt.Sprintf(
    "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
    cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.DbName,
)), &gorm.Config{
    MaxIdleConns: cfg.MaxIdleConns,
    MaxOpenConns: cfg.MaxOpenConns,
})
场景 2:配额检查
storageCache := storage.NewCache(prov)

cfg, _ := storageCache.GetByID(ctx, tenantID)
if file.Size > cfg.MaxFileSizeBytes {
    return errors.New("文件超过大小限制")
}

// 检查并发上传
if activeUploads >= cfg.MaxConcurrentUploads {
    return errors.New("已达到最大并发上传数")
}
场景 3:租户状态验证
if !prov.IsTenantEnabled(tenantID) {
    return errors.New("租户未激活")
}
场景 4:中间件集成

租户模块包含一个 Gin 中间件,用于从 HTTP 请求中提取租户 ID。支持四种解析方式(httpType):

httpType 说明 示例
header 从 HTTP Header 提取(默认) X-Tenant-ID: 123
query 从 URL 查询参数提取 ?tenant_id=123
path 从 URL 路径片段提取 /123/users
host 从域名/子域名提取 123.example.com
基本用法(无 Provider)
import "github.com/ChenBigdata421/jxt-core/sdk/pkg/tenant/middleware"

// 默认:从 X-Tenant-ID header 提取
router.Use(middleware.ExtractTenantID())

// 自定义 header 名称
router.Use(middleware.ExtractTenantID(
    middleware.WithHeaderName("X-Tenant-Id"),
))

// 查询参数提取
router.Use(middleware.ExtractTenantID(
    middleware.WithResolverType("query"),
    middleware.WithQueryParam("tenant_id"),
))

// 基于路径提取(提取第一个片段)
router.Use(middleware.ExtractTenantID(
    middleware.WithResolverType("path"),
    middleware.WithPathIndex(0),
))

// 基于域名提取(仅数字子域名,无 Provider)
router.Use(middleware.ExtractTenantID(
    middleware.WithResolverType("host"),
))
集成 Provider(推荐)

当传入 WithProviderConfig(provider) 时,中间件会自动从 ETCD 读取全部配置(httpTypehttpHeaderNamehttpQueryParamhttpPathIndexhttpHostMode),无需硬编码:

// 推荐:从 ETCD 自动读取全部配置
router.Use(middleware.ExtractTenantID(
    middleware.WithProviderConfig(provider),
))

ProviderConfigurer 接口

WithProviderConfig 需要传入实现了 ProviderConfigurer 接口的对象。该接口组合了三个子接口:

type ProviderConfigurer interface {
    DomainLookuper         // GetTenantIDByDomain(domain string) (int, bool)
    CodeLookuper           // GetTenantIDByCode(code string) (int, bool)
    ResolverConfigProvider // GetResolverConfig() *ResolverConfig
}

provider.Provider 已隐式实现该接口(鸭子类型),可直接使用。

host 模式的三种子模式(httpHostMode)

httpType = "host" 时,支持三种互斥的子模式:

httpHostMode 说明 示例 需要 Provider
numeric(默认) 仅数字子域名 123.example.com123
domain 精确域名匹配 tenant1.example.com → 查域名索引
code 租户代码匹配 acmecorp.example.com → 查代码索引

配置位置:ETCD common/resolver 路径,由 tenant-service 管理

{
  "httpType": "host",
  "httpHostMode": "code"
}
在处理器中获取租户 ID
func handler(c *gin.Context) {
    // 获取租户 ID(返回 int,未找到返回 0)
    tenantID := middleware.GetTenantID(c)

    // 或带错误返回
    tenantIDInt, err := middleware.GetTenantIDAsInt(c)
    if err != nil {
        // 处理错误
    }

    // 或直接 panic(适用于必须要有租户的场景)
    tenantID := middleware.MustGetTenantID(c)
}
场景 5:服务级数据库配置与密码解密

从 jxt-core Provider 获取的 ServiceDatabaseConfig 包含加密后的数据库密码。要使用密码建立连接,需要先解密:

import (
    "fmt"
    "log"
    "os"

    "github.com/ChenBigdata421/jxt-core/sdk/pkg/tenant/provider"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func ConnectToServiceDatabase(prov *provider.Provider, tenantID int, serviceCode string) (*gorm.DB, error) {
    // 从环境变量获取加密密钥
    encryptionKey := os.Getenv("ENCRYPTION_KEY")
    if encryptionKey == "" {
        return nil, fmt.Errorf("ENCRYPTION_KEY environment variable not set")
    }

    // 获取服务级数据库配置
    config, ok := provider.GetServiceDatabaseConfig(tenantID, serviceCode)
    if !ok {
        return nil, fmt.Errorf("database config not found for tenant %d, service %s", tenantID, serviceCode)
    }

    // 检查是否有加密密码
    if !config.HasEncryptedPassword() {
        return nil, fmt.Errorf("no encrypted password in config")
    }

    // 解密密码
    password, err := config.DecryptPassword(encryptionKey)
    if err != nil {
        return nil, fmt.Errorf("failed to decrypt password: %w", err)
    }

    // 构建数据库连接
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        config.Username, password, config.Host, config.Port, config.Database)

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, fmt.Errorf("failed to connect to database: %w", err)
    }

    return db, nil
}

重要提示

  • 加密密钥(ENCRYPTION_KEY)必须是 32 字节长度
  • 所有服务必须使用相同的加密密钥
  • 密钥应该通过安全的方式管理(如 Kubernetes Secrets、环境变量等)
  • 有关服务级数据库配置的更多详细信息,请参考 数据库配置文档

配置说明

环境变量
变量 默认值 描述
TENANT_CACHE_PATH ./cache/ 本地缓存文件目录
ETCD_ENDPOINTS localhost:2379 ETCD 服务地址
TENANT_NAMESPACE jxt/ ETCD 命名空间前缀
ETCD 数据结构
jxt/tenants/{id}/meta        → {"id":1,"code":"tenant1","name":"租户1","status":"active"}
jxt/tenants/{id}/database    → {"host":"localhost","port":3306,...}
jxt/tenants/{id}/ftp         → {"username":"ftp1","passwordHash":"...",...}
jxt/tenants/{id}/storage     → {"uploadQuotaGb":100,"maxFileSizeMb":500,...}

可靠性特性

1. 自动重连
  • 指数退避:1s → 30s(最大)
  • 重置条件:成功处理事件后退避重置为 1s
  • 所有重连事件:记录日志供监控
2. 初始化重试
操作 重试次数 间隔 总耗时
LoadAll 5 1s→2s→4s→8s→16s ~31s
StartWatch 3 1s→2s→5s ~8s
3. 本地缓存降级
ETCD 可用:  ETCD Watch → 内存 → 本地文件(同步)
ETCD 不可用: 本地文件 → 内存(只读模式)
4. 优雅关闭
defer prov.StopWatch()  // 停止监听,刷新缓存

故障排查

问题 1:ETCD 连接失败

症状failed to load tenant data after retries

排查步骤

# 检查 ETCD 服务
curl http://localhost:2379/health

# 检查租户数据
etcdctl get "" --prefix --keys-only

解决方案

  • 启用本地缓存降级(WithCache()
  • 检查网络连接
  • 查看重连日志
问题 2:配置未更新

症状:tenant-service 已更新,但微服务未感知

排查步骤

// 检查 watch 是否运行
if !prov.running.Load() {
    log.Error("Provider 未运行")
}

解决方案

  • 确认 StartWatch() 成功
  • 检查 ETCD 事件传递
  • 查看重连日志
问题 3:本地缓存文件损坏

症状invalid cache file format

解决方案

// 删除缓存文件,下次启动会重新生成
os.Remove("./cache/tenant_metadata.json")

扩展指南

添加新的配置类型
  1. 定义 ConfigType:
const ConfigTypeCustom ConfigType = "custom"
  1. 定义配置模型:
type CustomConfig struct {
    TenantID int    `json:"tenantId"`
    // ...
}
  1. 扩展 processKey/handleDeleteKey:
case "custom":
    p.processCustomKey(tenantID, parts, value, data)
  1. 创建 Cache:
// cache/custom/cache.go
type Cache struct { provider *provider.Provider }

func (c *Cache) GetByID(ctx, tenantID int) (*CustomConfig, error) {
    // ...
}

性能指标

指标 数值 描述
读取延迟 ~25ns atomic.Value 无锁读取
更新延迟 ~1μs Copy-on-Write
内存占用 ~1KB/租户 包含所有配置类型
Watch 同步 <1s ETCD 推送延迟

相关文档


许可证

Copyright © 2026 JXT-Evidence-System

Directories

Path Synopsis
Package cache provides persistent file-based caching for tenant configuration.
Package cache provides persistent file-based caching for tenant configuration.
Package middleware provides tenant ID extraction middleware for Gin framework.
Package middleware provides tenant ID extraction middleware for Gin framework.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL