From 75d5c9fea57855dd2ef83ad279d8b827efd6b7cb Mon Sep 17 00:00:00 2001 From: xofine Date: Wed, 19 Nov 2025 23:15:17 -0500 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=20ARCHITECTURE.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ARCHITECTURE.md | 143 ------------------------------------------------ 1 file changed, 143 deletions(-) delete mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index eddf543..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,143 +0,0 @@ -# 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` 模型。通过精心设计的缓存架构、清晰的分层重构、可靠的数据迁移方案以及最终的性能与健壮性优化,新系统不仅解决了旧架构的核心痛点,还在性能、稳定性和可维护性方面达到了更高的标准。 \ No newline at end of file