4. 报表数据源抽象:定义统一的 DataFetcher 接口
在报表回放缓存一致性体系中,数据源是缓存的下游依赖。不同报表可能从数据库、REST API、文件系统甚至消息队列中获取原始数据。如果每个报表直接耦合具体数据源,缓存层将无法统一管理回放与失效逻辑。本章引入 DataFetcher 接口,将数据源访问抽象为统一契约,使缓存中间件可以透明地调用任意后端。
4.1 为什么需要数据源抽象
未抽象的数据源在报表系统中会引发以下问题:
- 缓存逻辑重复:每个报表都要自行实现缓存键拼接、回放触发、超时处理。
- 回放路径混乱:数据库查询失败时回退到缓存、API 限流时切换本地文件——这些策略散落在业务代码中。
- 测试与 Mock 困难:单元测试需要真实数据库或外部服务,无法快速构造数据源桩。
抽象目标:让缓存层只依赖
DataFetcher 接口,不关心背后是 MySQL、Redis、CSV 还是第三方 HTTP 服务。
4.2 DataFetcher 接口定义
在 Go 中,接口应足够小且内聚。核心方法只有两个:Fetch 用于获取原始数据,Key 用于生成缓存键前缀。
// DataFetcher 定义报表数据源的统一访问契约。
type DataFetcher interface {
// Fetch 根据上下文和请求参数返回原始数据。
// ctx 携带超时、追踪信息;params 是报表筛选条件(如时间范围、维度)。
Fetch(ctx context.Context, params map[string]interface{}) (interface{}, error)
// Key 返回该数据源的唯一标识前缀,用于构建缓存键。
// 例如 "sales:db:orders" 或 "inventory:api:stock"。
Key() string
// TTL 建议缓存过期时间(秒)。0 表示使用系统默认。
TTL() time.Duration
}
| 方法 | 输入 | 输出 | 职责 |
|---|---|---|---|
Fetch |
ctx, params |
data, error |
执行实际数据获取(SQL 查询、HTTP 调用、文件读取) |
Key |
无 | string |
返回数据源类型+标识,用于缓存键命名空间 |
TTL |
无 | time.Duration |
建议缓存时长,允许缓存层做优先级决策 |
4.3 三种典型实现
4.3.1 数据库实现(MySQL)
type MySQLFetcher struct {
db *sql.DB
query string
prefix string
ttl time.Duration
}
func (m *MySQLFetcher) Fetch(ctx context.Context, params map[string]interface{}) (interface{}, error) {
rows, err := m.db.QueryContext(ctx, m.query, params["start"], params["end"])
if err != nil {
return nil, fmt.Errorf("mysql fetch: %w", err)
}
defer rows.Close()
// 解析 rows 为 []map[string]interface{} 或结构体切片
return parseRows(rows)
}
func (m *MySQLFetcher) Key() string { return m.prefix }
func (m *MySQLFetcher) TTL() time.Duration { return m.ttl }
4.3.2 API 实现(HTTP)
type APIFetcher struct {
baseURL string
endpoint string
apiKey string
prefix string
ttl time.Duration
httpClient *http.Client
}
func (a *APIFetcher) Fetch(ctx context.Context, params map[string]interface{}) (interface{}, error) {
url := fmt.Sprintf("%s%s?date=%s", a.baseURL, a.endpoint, params["date"])
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+a.apiKey)
resp, err := a.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("api fetch: %w", err)
}
defer resp.Body.Close()
var result interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
func (a *APIFetcher) Key() string { return a.prefix }
func (a *APIFetcher) TTL() time.Duration { return a.ttl }
4.3.3 文件实现(CSV)
type FileFetcher struct {
filePath string
prefix string
ttl time.Duration
}
func (f *FileFetcher) Fetch(ctx context.Context, params map[string]interface{}) (interface{}, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
file, err := os.Open(f.filePath)
if err != nil {
return nil, fmt.Errorf("file fetch: %w", err)
}
defer file.Close()
reader := csv.NewReader(file)
records, _ := reader.ReadAll()
return records, nil
}
func (f *FileFetcher) Key() string { return f.prefix }
func (f *FileFetcher) TTL() time.Duration { return f.ttl }
4.4 接口组合与扩展点
实际报表场景中,数据源可能需要额外能力。推荐通过接口组合而非修改核心契约:
// BatchFetcher 支持批量获取,用于预加载多个报表。
type BatchFetcher interface {
DataFetcher
FetchBatch(ctx context.Context, paramsList []map[string]interface{}) ([]interface{}, error)
}
// HealthCheckFetcher 支持健康检查,用于缓存预热前探测数据源可用性。
type HealthCheckFetcher interface {
DataFetcher
Ping(ctx context.Context) error
}
注意:不要将
Ping、Close、Stats 等方法塞入基础 DataFetcher 接口。保持接口最小化,通过类型断言在缓存层按需使用扩展接口。
4.5 数据源抽象架构图
下图展示 DataFetcher 接口如何将缓存层与具体后端解耦:
4.6 接口设计决策对比
| 设计维度 | 推荐方案 | 反模式 | 理由 |
|---|---|---|---|
| 方法数量 | 3 个(Fetch, Key, TTL) | 超过 5 个方法 | 接口越大,实现负担越重,违反接口隔离原则 |
| 参数传递 | map[string]interface{} |
强类型结构体 | 不同数据源参数差异大,map 提供灵活性;内部可做类型校验 |
| 返回值 | interface{} |
[]byte 或 string |
缓存层只负责存储,不关心序列化格式;由具体 Fetcher 决定 |
| 错误处理 | 返回 error,包含上下文 |
吞掉错误或返回空数据 | 缓存层需要根据错误类型决定是否回放、降级或告警 |
| TTL 归属 | 数据源建议,缓存层决策 | 数据源强制 TTL | 缓存层可能根据全局策略(如内存压力)调整过期时间 |
4.7 在缓存层中使用 DataFetcher
缓存中间件通过 DataFetcher 接口统一调用,无需感知具体后端:
func (c *Cache) GetOrFetch(ctx context.Context, fetcher DataFetcher, params map[string]interface{}) (interface{}, error) {
key := buildCacheKey(fetcher.Key(), params)
// 1. 尝试从缓存读取
if val, ok := c.local.Get(key); ok {
return val, nil
}
// 2. 通过 DataFetcher 回放数据源
data, err := fetcher.Fetch(ctx, params)
if err != nil {
return nil, fmt.Errorf("fetch failed: %w", err)
}
// 3. 写入缓存
c.local.Set(key, data, fetcher.TTL())
return data, nil
}
关键收益:新增一个报表数据源时,只需编写一个实现
DataFetcher 的结构体,注册到缓存层即可。缓存回放、失效、预热逻辑完全复用,无需改动任何缓存代码。
4.8 本章小结
- DataFetcher 接口 定义了
Fetch、Key、TTL三个方法,覆盖数据获取、缓存键生成、过期建议三大职责。 - 三种典型实现(MySQL、API、文件)展示了接口的普适性,实际项目可扩展至 MongoDB、Redis、Kafka 等。
- 接口组合 通过
BatchFetcher、HealthCheckFetcher等扩展接口满足高级场景,不污染基础契约。 - 缓存层解耦 后,新增数据源的成本从“修改缓存逻辑”降为“实现三个方法”,为后续章节的缓存一致性策略奠定基础。