|

Aimee

Write the Code. Change the World.

报表可重跑的设计:快照、水位线与幂等

· 分享镜

报表可重跑的设计:快照、水位线与幂等

「同一张报表,昨天跑出来的数和今天重跑的不一样」——这类问题的原因通常不是计算 bug,而是两次计算之间有数据补录了进来:报表第一次跑的时候这批数据还不在,重跑就多了。

这暴露的是设计问题:报表要不要支持重跑?重跑结果要和原来一样,还是允许更新?


幂等计算的前提:数据快照

如果要求"重跑结果和第一次一样",那就需要在任务第一次跑之前,给数据打快照——基于快照计算,而不是基于当前实时数据。

public void runDailyReport(LocalDate date) {
    // 先检查:今天的快照是否已存在
    if (snapshotDao.exists(date)) {
        log.info("Snapshot exists for {}, skip snapshot creation", date);
    } else {
        // 第一次跑:把当天的原始数据快照到报表库
        snapshotService.createSnapshot(date);
    }
    
    // 基于快照计算,无论重跑多少次结果都一样
    List<RawRecord> records = snapshotDao.queryByDate(date);
    ReportResult result = calculate(records);
    reportDao.upsert(date, result);
}

快照一旦创建就不再修改,后续补录的数据进不了这份快照,重跑结果天然幂等。


增量计算的水位线问题

另一类报表是增量的:每次只处理"上次处理到哪里"之后的新数据,用水位线(watermark)或游标记录进度:

public void processIncremental() {
    long lastId = watermarkDao.getLastProcessedId("order_report");
    List<Order> batch = orderDao.findAfter(lastId, 1000);
    
    if (batch.isEmpty()) return;
    
    // 处理这批数据
    process(batch);
    
    // 更新水位线
    watermarkDao.update("order_report", batch.get(batch.size() - 1).getId());
}

增量计算的陷阱:水位线是按 ID(自增)还是按时间?

按时间水位线容易出问题:WHERE created_at > lastWatermark 会漏掉迟到数据(网络延迟、批量补录、时钟不一致导致的数据乱序)。

按 ID 水位线相对可靠(自增 ID 单调递增),但补录历史数据时 ID 在水位线之前,一样会漏。


增量 vs 全量

增量全量
计算量小,只处理新数据大,每次重算所有数据
迟到数据处理复杂,需补偿机制自然覆盖
重跑需要回退水位线直接重跑
适合场景数据量大、实时性高数据量可接受、准确性优先

"昨天的数和今天重跑对不上",通常是用了增量计算但没有处理迟到数据。解法有两种:

  1. 全量重算:接受计算代价,每次跑T日报表就重算T日全部数据
  2. 迟到数据补偿:增量计算 + 单独的补偿逻辑——迟到数据入库后,触发对应日期的报表重跑

时区和"昨天"的边界

还有一类对不上是时区导致的:服务器存的是 UTC,报表按北京时间的"昨天"切割,边界处的数据因为时区换算错误被切到了错的天。

报表时间边界要明确定义并写在代码注释里:

// 以北京时间 00:00:00 为日边界,转成 UTC 后是前一天 16:00:00
LocalDate reportDate = LocalDate.now(ZoneId.of("Asia/Shanghai")).minusDays(1);
ZonedDateTime start = reportDate.atStartOfDay(ZoneId.of("Asia/Shanghai")).toInstant();
ZonedDateTime end = reportDate.plusDays(1).atStartOfDay(ZoneId.of("Asia/Shanghai")).toInstant();

时区错误是低级但高频的坑,尤其在有海外用户或多机房的系统里。

评论0

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部