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 接口如何将缓存层与具体后端解耦:

DataFetcher 接口抽象架构 DataFetcher 接口解耦缓存层与数据源 缓存回放中间件 <<interface>> DataFetcher MySQLFetcher APIFetcher FileFetcher MySQL / PostgreSQL REST / gRPC API CSV / Parquet 文件 缓存层通过 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 等扩展接口满足高级场景,不污染基础契约。
  • 缓存层解耦 后,新增数据源的成本从“修改缓存逻辑”降为“实现三个方法”,为后续章节的缓存一致性策略奠定基础。