7.8 KiB
M:N 架构升级与性能优化说明书
1. 引言与背景
1.1 初始状态
项目初期,API 密钥(APIKey)与密钥组(KeyGroup)之间采用的是 一对多 (1:N) 的数据模型。在这种设计下,一个 APIKey 实体通过 group_id 字段直接隶属于一个 KeyGroup。
1.2 核心痛点
随着业务发展,1:N 模型的局限性日益凸显:
- 数据冗余: 同一个 API 密钥若需在不同场景(分组)下使用,必须在数据库中创建多条重复的记录,造成存储浪费。
- 管理复杂: 更新一个通用密钥的状态需要在多个副本之间同步,操作繁琐且容易出错。
- 统计失真: 无法准确统计一个物理密钥的真实使用情况,因为它在系统中表现为多个独立实体。
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 模型进行了相应调整:
// 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): 一个
APIKeyID 的列表。 - 作用: 这是 API 代理服务的核心读路径。通过对该列表执行
LMOVE(由store.Rotate封装) 原子操作,我们可以实现高效的、轮询式的 (Round-Robin) 活跃密钥选择。
3.3 反向索引 (SET)
- 键 (Key):
key:{id}:groups - 值 (Value): 一个包含所有关联
KeyGroupID 的集合。 - 作用: 这是
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的查询,现在都改为通过JOINkeygroup_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 脚本。
- 核心逻辑:
- 创建新表: 自动创建
keygroup_apikey_mappings关联表。 - 去重
apikeys: 遍历旧的apikeys表,将重复的api_key值合并为单个实体,并记录旧 ID 到新 ID 的映射。 - 填充关联表: 根据旧
apikeys表中的group_id和api_key值,在新keygroup_apikey_mappings表中创建正确的关联关系。 - 清理旧数据: 删除旧的、有冗余的
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 异步状态更新与事务重试
- 问题:
- 来自管理后台的手动状态更新是同步阻塞的,影响 UI 响应速度。
- 高并发下,数据库事务可能因
database is locked等瞬时错误而失败。
- 解决方案:
- 异步化: 我们将
APIKeyService中的UpdateAPIKey方法的核心逻辑移入一个后台goroutine,使其成为一个“即发即忘”的非阻塞操作。 - 事务重试: 我们在
repository层实现了一个executeTransactionWithRetry辅助函数,当事务遇到可重试的锁错误时,会进行带随机抖动 (Jitter) 的延时重试,极大地增强了系统的健壮性。
- 异步化: 我们将
7. 总结
本次架构升级成功地将系统从受限的 1:N 模型演进为灵活、可扩展的 M:N 模型。通过精心设计的缓存架构、清晰的分层重构、可靠的数据迁移方案以及最终的性能与健壮性优化,新系统不仅解决了旧架构的核心痛点,还在性能、稳定性和可维护性方面达到了更高的标准。