1
0

feat(cache): 实现 RoutingCache 和 StatsBuffer 优化数据库写入

- 新增 RoutingCache 组件,使用 sync.Map 缓存 Provider 和 Model
- 新增 StatsBuffer 组件,使用 sync.Map + atomic.Int64 缓冲统计数据
- 扩展 StatsRepository.BatchUpdate 支持批量增量更新
- 改造 RoutingService/StatsService/ProviderService/ModelService 集成缓存
- 更新 usage-statistics spec,新增 routing-cache 和 stats-buffer spec
- 新增单元测试覆盖缓存命中/失效/并发场景
This commit is contained in:
2026-04-22 19:24:36 +08:00
parent f5e45d032e
commit df253559a5
20 changed files with 1377 additions and 91 deletions

View File

@@ -0,0 +1,136 @@
# Routing Cache
## Purpose
TBD - 为 Provider 和 Model 提供内存缓存,优化路由查询性能。
## Requirements
### Requirement: RoutingCache 缓存 Provider 和 Model
系统 SHALL 为 Provider 和 Model 提供内存缓存,使用 sync.Map 作为缓存数据结构。
#### Scenario: 缓存数据结构
- **WHEN** 创建 RoutingCache
- **THEN** SHALL 使用 sync.Map 存储 Providerkey = providerID
- **THEN** SHALL 使用 sync.Map 存储 Modelkey = providerID/modelName
### Requirement: 缓存查询优先
RoutingCache SHALL 优先从缓存查询,缓存未命中时查询数据库并更新缓存。
#### Scenario: 缓存命中
- **WHEN** 查询 Provider 或 Model
- **THEN** SHALL 先从缓存查询
- **THEN** 如果缓存命中SHALL 直接返回缓存值
- **THEN** SHALL 不查询数据库
#### Scenario: 缓存未命中
- **WHEN** 查询 Provider 或 Model
- **THEN** SHALL 先从缓存查询
- **THEN** 如果缓存未命中SHALL 查询数据库
- **THEN** SHALL 将查询结果存入缓存
- **THEN** SHALL 返回查询结果
#### Scenario: 双重检查防止竞态
- **WHEN** 并发查询同一个 Provider 或 Model
- **THEN** SHALL 在查询数据库后再次检查缓存
- **THEN** 如果已有其他 goroutine 存入缓存SHALL 使用缓存值
- **THEN** SHALL 防止存入旧值
### Requirement: 缓存更新
RoutingCache SHALL 支持 Create 操作后更新缓存。
#### Scenario: Create Provider 后更新缓存
- **WHEN** 创建 Provider 成功
- **THEN** SHALL 调用 RoutingCache.SetProvider
- **THEN** SHALL 将新 Provider 存入缓存
#### Scenario: Create Model 后更新缓存
- **WHEN** 创建 Model 成功
- **THEN** SHALL 调用 RoutingCache.SetModel
- **THEN** SHALL 将新 Model 存入缓存
### Requirement: 缓存失效
RoutingCache SHALL 支持 Update/Delete 操作后清除缓存。
#### Scenario: Update Provider 后清除缓存
- **WHEN** 更新 Provider 成功
- **THEN** SHALL 调用 RoutingCache.InvalidateProvider
- **THEN** SHALL 清除该 Provider 的缓存
- **THEN** SHALL 级联清除该 Provider 的所有 Model 缓存
#### Scenario: Delete Provider 后清除缓存
- **WHEN** 删除 Provider 成功
- **THEN** SHALL 调用 RoutingCache.InvalidateProvider
- **THEN** SHALL 清除该 Provider 的缓存
- **THEN** SHALL 级联清除该 Provider 的所有 Model 缓存
#### Scenario: Update Model 后清除缓存
- **WHEN** 更新 Model 成功
- **THEN** SHALL 调用 RoutingCache.InvalidateModel
- **THEN** SHALL 清除旧位置的 Model 缓存
- **THEN** 如果 provider_id 或 model_name 变化SHALL 也清除新位置的缓存
#### Scenario: Delete Model 后清除缓存
- **WHEN** 删除 Model 成功
- **THEN** SHALL 调用 RoutingCache.InvalidateModel
- **THEN** SHALL 清除该 Model 的缓存
### Requirement: 缓存预热
RoutingCache SHALL 支持启动时预热缓存。
#### Scenario: 预热成功
- **WHEN** 服务启动时
- **THEN** SHALL 调用 RoutingCache.Preload
- **THEN** SHALL 从数据库加载所有 Provider 到缓存
- **THEN** SHALL 从数据库加载所有 Model 到缓存
- **THEN** SHALL 记录预热完成的日志
#### Scenario: 预热失败
- **WHEN** 预热失败时
- **THEN** SHALL 记录警告日志
- **THEN** SHALL 继续启动服务
- **THEN** SHALL 使用懒加载(首次查询时加载)
### Requirement: RoutingService 使用缓存
RoutingService SHALL 使用 RoutingCache 进行路由查询。
#### Scenario: RouteByModelName 使用缓存
- **WHEN** 调用 RoutingService.RouteByModelName
- **THEN** SHALL 调用 RoutingCache.GetModel 获取 Model
- **THEN** SHALL 调用 RoutingCache.GetProvider 获取 Provider
- **THEN** SHALL 不直接调用 Repository
### Requirement: 并发安全
RoutingCache SHALL 支持并发访问。
#### Scenario: 并发查询
- **WHEN** 多个 goroutine 并发查询缓存
- **THEN** SHALL 无竞态条件
- **THEN** SHALL 无 panic
#### Scenario: 并发查询和失效
- **WHEN** 并发查询和失效缓存
- **THEN** SHALL 无竞态条件
- **THEN** SHALL 保证一致性

View File

@@ -0,0 +1,156 @@
# Stats Buffer
## Purpose
TBD - 为统计数据提供内存缓冲,优化写入性能。
## Requirements
### Requirement: StatsBuffer 内存缓冲
系统 SHALL 为统计数据提供内存缓冲,使用 sync.Map + atomic.Int64 进行计数。
#### Scenario: 缓冲数据结构
- **WHEN** 创建 StatsBuffer
- **THEN** SHALL 使用 sync.Map 存储计数器key = providerID/modelName/date
- **THEN** SHALL 使用 atomic.Int64 进行计数
- **THEN** SHALL 支持配置刷新间隔和阈值
### Requirement: 原子计数
StatsBuffer SHALL 使用原子操作进行计数。
#### Scenario: Increment 计数
- **WHEN** 调用 StatsBuffer.Increment
- **THEN** SHALL 使用 atomic.AddInt64 增加计数
- **THEN** SHALL 无锁操作
- **THEN** SHALL 线程安全
#### Scenario: Increment 创建计数器
- **WHEN** 调用 StatsBuffer.Increment 且计数器不存在
- **THEN** SHALL 使用 sync.Map.LoadOrStore 创建计数器
- **THEN** SHALL 初始化计数器为 0
- **THEN** SHALL 原子增加计数
#### Scenario: 并发计数
- **WHEN** 多个 goroutine 并发 Increment
- **THEN** SHALL 计数准确
- **THEN** SHALL 无竞态条件
### Requirement: 定时刷新
StatsBuffer SHALL 支持定时刷新到数据库。
#### Scenario: 定时刷新触发
- **WHEN** 后台刷新协程运行
- **THEN** SHALL 每隔 flushInterval 触发刷新
- **THEN** SHALL 调用 StatsRepository.BatchUpdate 写入数据库
#### Scenario: 刷新间隔配置
- **WHEN** 创建 StatsBuffer
- **THEN** 默认 flushInterval 为 5 秒
- **THEN** 可通过 WithFlushInterval 选项配置
### Requirement: 阈值触发刷新
StatsBuffer SHALL 支持累计阈值触发刷新。
#### Scenario: 阈值触发
- **WHEN** 累计计数达到 flushThreshold
- **THEN** SHALL 异步触发刷新
- **THEN** SHALL 不阻塞请求
#### Scenario: 阈值配置
- **WHEN** 创建 StatsBuffer
- **THEN** 默认 flushThreshold 为 100
- **THEN** 可通过 WithFlushThreshold 选项配置
### Requirement: 批量写入数据库
StatsBuffer SHALL 批量写入统计数据到数据库。
#### Scenario: 批量写入
- **WHEN** 刷新触发
- **THEN** SHALL 遍历所有计数器
- **THEN** SHALL 使用 atomic.SwapInt64(counter, 0) 获取并清零计数器
- **THEN** SHALL 调用 StatsRepository.BatchUpdate 批量写入
- **THEN** SHALL 重置 totalCount 为 0
#### Scenario: SwapInt64 清零计数器
- **WHEN** flush 收集计数器
- **THEN** SHALL 使用 SwapInt64 原子操作获取当前计数并清零
- **THEN** SHALL 保证计数不丢失(新计数会累加到已清零的计数器)
- **THEN** SHALL 不阻塞后续 Increment 操作
#### Scenario: 写入失败保留计数器
- **WHEN** BatchUpdate 失败
- **THEN** SHALL 将计数加回计数器(使用 atomic.AddInt64
- **THEN** SHALL 记录错误日志
- **THEN** SHALL 继续处理其他条目
### Requirement: 优雅关闭
StatsBuffer SHALL 支持优雅关闭,确保最后的统计写入数据库。
#### Scenario: Stop 等待刷新完成
- **WHEN** 调用 StatsBuffer.Stop
- **THEN** SHALL 停止后台刷新协程
- **THEN** SHALL 执行最后一次刷新
- **THEN** SHALL 等待刷新完成
- **THEN** SHALL 保证统计数据不丢失
#### Scenario: 无超时
- **WHEN** Stop 等待刷新
- **THEN** SHALL 无超时限制
- **THEN** SHALL 等待刷新完成
### Requirement: StatsService 使用缓冲
StatsService SHALL 使用 StatsBuffer 进行统计记录。
#### Scenario: Record 使用缓冲
- **WHEN** 调用 StatsService.Record
- **THEN** SHALL 调用 StatsBuffer.Increment
- **THEN** SHALL 不直接调用 StatsRepository.Record
- **THEN** SHALL 立即返回,不阻塞
### Requirement: StatsRepository 扩展
StatsRepository SHALL 新增 BatchUpdate 方法。
#### Scenario: BatchUpdate 方法
- **WHEN** 调用 StatsRepository.BatchUpdate
- **THEN** SHALL 使用事务更新或创建统计记录
- **THEN** SHALL 使用 request_count + delta 更新
- **THEN** SHALL 支持批量增量更新
### Requirement: 并发安全
StatsBuffer SHALL 支持并发访问。
#### Scenario: 并发 Increment 和 flush
- **WHEN** 并发 Increment 和 flush
- **THEN** SHALL 无竞态条件
- **THEN** SHALL 计数准确(可能延迟到下次 flush
#### Scenario: flush 期间 Increment
- **WHEN** flush 正在执行
- **THEN** 新的 Increment SHALL 继续计数
- **THEN** SHALL 不会丢失计数

View File

@@ -13,14 +13,15 @@
#### Scenario: 记录成功请求
- **WHEN** 请求成功转发到供应商
- **THEN** 网关 SHALL 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 记录当前日期的统计
- **THEN** 网关 SHALL 通过 StatsBuffer 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 异步批量写入数据库(定时或阈值触发)
- **THEN** 网关 SHALL 不阻塞响应
#### Scenario: 记录流式请求
- **WHEN** 流式请求成功完成
- **THEN** 网关 SHALL 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 在流结束后记录统计
- **THEN** 网关 SHALL 通过 StatsBuffer 增加该供应商和模型的请求计数
- **THEN** 网关 SHALL 在流结束后异步记录统计
### Requirement: 使用统计记录统一模型标识
@@ -90,10 +91,9 @@
#### Scenario: 并发请求
- **WHEN** 同时处理多个并发请求
- **THEN** 网关 SHALL 正确为每个请求增加请求计数
- **THEN** 网关 SHALL 使用原子操作正确增加每个请求的计数
- **THEN** 不 SHALL 因并发写入而丢失统计
**变更说明:** 并发控制在 StatsRepository 中通过数据库事务实现。API 接口保持不变。
- **THEN** SHALL 使用 StatsBuffer 的内存计数器
### Requirement: 使用 service 层处理业务逻辑
@@ -121,6 +121,13 @@ Service SHALL 通过 StatsRepository 访问数据。
- **THEN** SHALL 调用对应的 StatsRepository 方法
- **THEN** SHALL 使用 domain.UsageStats 类型
#### Scenario: 批量更新统计
- **WHEN** StatsBuffer 刷新统计
- **THEN** SHALL 调用 StatsRepository.BatchUpdate
- **THEN** SHALL 使用事务更新或创建统计记录
- **THEN** SHALL 支持增量更新request_count + delta
#### Scenario: 事务处理
- **WHEN** 记录统计
@@ -136,3 +143,36 @@ Service SHALL 通过 StatsRepository 访问数据。
- **WHEN** 查询统计
- **THEN** SHALL 使用 (provider_id, model_name, date) 复合索引
- **THEN** SHALL 优化查询性能
### Requirement: 统计数据可接受少量丢失
统计记录方式改为内存缓冲,可接受少量丢失。
#### Scenario: 进程崩溃丢失统计
- **WHEN** 进程崩溃
- **THEN** MAY 丢失最近 flushInterval 内的统计
- **THEN** 统计数据用于监控,可接受少量丢失
#### Scenario: 优雅关闭保证不丢失
- **WHEN** 服务优雅关闭
- **THEN** SHALL 调用 StatsBuffer.Stop
- **THEN** SHALL 等待最后一次刷新完成
- **THEN** SHALL 保证统计数据不丢失
### Requirement: StatsRepository 支持批量更新
StatsRepository SHALL 新增 BatchUpdate 方法支持批量增量更新。
#### Scenario: BatchUpdate 更新现有记录
- **WHEN** 调用 BatchUpdate 且当日记录存在
- **THEN** SHALL 使用事务更新 request_count = request_count + delta
- **THEN** SHALL 不创建新记录
#### Scenario: BatchUpdate 创建新记录
- **WHEN** 调用 BatchUpdate 且当日记录不存在
- **THEN** SHALL 创建新记录request_count = delta
- **THEN** SHALL 使用事务保证原子性