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

143 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 模型通过引入一个中间关联表来解除 `APIKey``KeyGroup` 之间的直接耦合。
* **`apikeys` 表**: 只存储 `APIKey` 的唯一信息(如 `api_key` 值、状态、错误统计等),不再包含 `group_id` 字段。
* **`key_groups` 表**: 保持不变,存储分组信息。
* **`keygroup_apikey_mappings` 表 (新增)**: 这是实现 `M:N` 关系的核心。它包含 `key_group_id``api_key_id` 两个外键,用于记录 `APIKey``KeyGroup` 之间的关联关系。
### 2.2 GORM 模型调整
我们对 `internal/models/models.go` 中的 GORM 模型进行了相应调整:
```go
// 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/*.go``internal/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_id``api_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` 模型。通过精心设计的缓存架构、清晰的分层重构、可靠的数据迁移方案以及最终的性能与健壮性优化,新系统不仅解决了旧架构的核心痛点,还在性能、稳定性和可维护性方面达到了更高的标准。