Files
gemini-banlancer/ARCHITECTURE.md
2025-11-20 12:12:26 +08:00

7.8 KiB
Raw Blame History

M:N 架构升级与性能优化说明书

1. 引言与背景

1.1 初始状态

项目初期API 密钥(APIKey)与密钥组(KeyGroup)之间采用的是 一对多 (1:N) 的数据模型。在这种设计下,一个 APIKey 实体通过 group_id 字段直接隶属于一个 KeyGroup

1.2 核心痛点

随着业务发展,1:N 模型的局限性日益凸显:

  1. 数据冗余: 同一个 API 密钥若需在不同场景(分组)下使用,必须在数据库中创建多条重复的记录,造成存储浪费。
  2. 管理复杂: 更新一个通用密钥的状态需要在多个副本之间同步,操作繁琐且容易出错。
  3. 统计失真: 无法准确统计一个物理密钥的真实使用情况,因为它在系统中表现为多个独立实体。

1.3 重构目标

为解决上述问题,我们启动了本次架构升级,核心目标是将数据模型从 1:N 重构为 多对多 (M:N)。这将允许一个 APIKey 实体被多个 KeyGroup 共享和复用,从根本上解决数据冗余和管理复杂性的问题。


2. 核心设计M:N 架构

2.1 新数据模型

新的 M:N 模型通过引入一个中间关联表来解除 APIKeyKeyGroup 之间的直接耦合。

  • apikeys: 只存储 APIKey 的唯一信息(如 api_key 值、状态、错误统计等),不再包含 group_id 字段。
  • key_groups: 保持不变,存储分组信息。
  • keygroup_apikey_mappings 表 (新增): 这是实现 M:N 关系的核心。它包含 key_group_idapi_key_id 两个外键,用于记录 APIKeyKeyGroup 之间的关联关系。

2.2 GORM 模型调整

我们对 internal/models/models.go 中的 GORM 模型进行了相应调整:

// APIKey 模型移除了 GroupID 字段
type APIKey struct {
    ID                    uint      `gorm:"primarykey"`
    APIKey                string    `gorm:"column:api_key;uniqueIndex;not null"`
    KeyGroups             []*KeyGroup `gorm:"many2many:keygroup_apikey_mappings;"` // 新增 M:N 关系定义
}

// KeyGroup 模型新增了 M:N 关系定义
type KeyGroup struct {
    ID        uint      `gorm:"primarykey"`
    Name      string    `gorm:"uniqueIndex;not null"`
    APIKeys   []*APIKey `gorm:"many2many:keygroup_apikey_mappings;"`
}

3. 高性能缓存架构

为了在 M:N 模型下依然保持极高的读写性能,我们设计了一套三层复合缓存结构,存储在 Redis (或内存 Store) 中。

3.1 对象缓存 (HASH)

  • 键 (Key): key:{id}
  • 值 (Value): APIKey 实体的序列化数据 (如 api_key 值, status 等)。
  • 作用: 提供对单个 APIKey 详细信息的 O(1) 快速访问。

3.2 正向索引 (LIST)

  • 键 (Key): group:{id}:keys:active
  • 值 (Value): 一个 APIKey ID 的列表。
  • 作用: 这是 API 代理服务的核心读路径。通过对该列表执行 LMOVE (由 store.Rotate 封装) 原子操作,我们可以实现高效的、轮询式的 (Round-Robin) 活跃密钥选择。

3.3 反向索引 (SET)

  • 键 (Key): key:{id}:groups
  • 值 (Value): 一个包含所有关联 KeyGroup ID 的集合。
  • 作用: 这是 M:N 架构下的关键设计。当一个 APIKey 的状态发生变化时(例如,从 active 变为 disabled),我们能通过此索引在 O(1) 时间内找到所有受影响的 KeyGroup,进而精确地更新相关的 group:{id}:keys:active 列表,实现了高效的缓存同步。

这套缓存架构确保了核心的密钥读取操作性能不受影响,同时优雅地解决了 M:N 模型下状态变更的“扇出”(fan-out) 更新难题。


4. 分层架构重构

为适配新的数据和缓存模型,我们对应用各层进行了自底向上的重构。

4.1 Repository 层

internal/repository/repository.go 作为数据访问层,改动最为核心:

  • 查询变更: 所有原先基于 group_id 的查询,现在都改为通过 JOIN keygroup_apikey_mappings 关联表来完成。
  • 新增 M:N 操作: 增加了如 LinkKeysToGroup, UnlinkKeysFromGroup, GetGroupsForKey 等直接操作关联关系的方法。
  • 缓存逻辑更新: LoadAllKeysToStore, updateStoreCacheForKey, removeStoreCacheForKey 等缓存辅助函数被重写,以正确地维护上述三种缓存结构。

4.2 Service 层

internal/service/*.go 作为业务逻辑层,主要改动集中在事件处理和状态同步:

  • 事件驱动: 当一个 APIKey 状态变更时,APIKeyService 会发布一个 KeyStatusChangedEvent 事件。
  • 状态同步: StatsService 等其他服务会订阅此事件,并根据事件内容更新自身的统计数据(如 group_stats),实现了服务间的解耦。
  • 扇出通知: publishStatusChangeEvents 辅助函数会利用缓存中的“反向索引”(key:{id}:groups),为所有受影响的 KeyGroup 发布状态变更事件。

4.3 Handler & Router 层

internal/handlers/*.gointernal/router/router.go 的改动相对较小,主要是适配 Service 层接口的变更,确保 API 的输入输出符合新的业务逻辑。


5. 数据迁移方案

1:N 迁移到 M:N 需要一次性的数据迁移。我们为此编写了 scripts/migrate_mn.go 脚本。

  • 核心逻辑:
    1. 创建新表: 自动创建 keygroup_apikey_mappings 关联表。
    2. 去重 apikeys: 遍历旧的 apikeys 表,将重复的 api_key 值合并为单个实体,并记录旧 ID 到新 ID 的映射。
    3. 填充关联表: 根据旧 apikeys 表中的 group_idapi_key 值,在新 keygroup_apikey_mappings 表中创建正确的关联关系。
    4. 清理旧数据: 删除旧的、有冗余的 apikeys 表,并将去重后的新表重命名。
  • 健壮性: 脚本被设计为幂等的,并包含多重检查,以防止在已迁移或部分迁移的数据库上重复执行。
  • 数据库兼容性: 脚本使用 GORM 的 clause 来构建插入语句,避免了因原生 SQL 语法差异导致的数据库兼容性问题(如 SQLite 和 PostgreSQL

6. 最终性能与健壮性优化

在完成核心重构后,我们借鉴业界最佳实践,实施了以下两项关键优化:

6.1 Redis Pipeline 优化

  • 问题: 在 LoadAllKeysToStore 或更新一个 Key 状态时,需要执行多个独立的 Redis 命令导致多次网络往返RTT
  • 解决方案: 我们在 store 抽象层引入了 Pipeliner 接口。在 repository 中,所有多步缓存操作(如 HSet + SAdd + LRem + LPush)都被重构为使用 Pipeline。这会将多个命令打包成一次网络请求,显著降低了批量操作的延迟。

6.2 异步状态更新与事务重试

  • 问题:
    1. 来自管理后台的手动状态更新是同步阻塞的,影响 UI 响应速度。
    2. 高并发下,数据库事务可能因 database is locked 等瞬时错误而失败。
  • 解决方案:
    1. 异步化: 我们将 APIKeyService 中的 UpdateAPIKey 方法的核心逻辑移入一个后台 goroutine,使其成为一个“即发即忘”的非阻塞操作。
    2. 事务重试: 我们在 repository 层实现了一个 executeTransactionWithRetry 辅助函数,当事务遇到可重试的锁错误时,会进行带随机抖动 (Jitter) 的延时重试,极大地增强了系统的健壮性。

7. 总结

本次架构升级成功地将系统从受限的 1:N 模型演进为灵活、可扩展的 M:N 模型。通过精心设计的缓存架构、清晰的分层重构、可靠的数据迁移方案以及最终的性能与健壮性优化,新系统不仅解决了旧架构的核心痛点,还在性能、稳定性和可维护性方面达到了更高的标准。