统计聚合表设计:唯一键防重、每日 Job 落库与趋势补零

发布时间:2026/8/20 11:06:36
统计聚合表设计:唯一键防重、每日 Job 落库与趋势补零 模块yudao-module-statistics关键类/脚本TradeStatisticsServiceImpl、TradeOrderStatisticsServiceImpl#fillTrendGaps、22-pro-statistics-performance.sql关键词统计聚合表设计、唯一键防重、趋势图补零、商城数据库设计摘要趋势图缺日、重复统计行、补跑失控是电商统计系统的三大经典事故。本文介绍久滴直播电商系统在 P0 统计修复中的落库层设计(tenant_id, time)唯一键从 DB 层杜绝重复、每日 Job 幂等落库、fillTrendGaps游标补零保证 X 轴连续以及补跑接口的天数上限防护。技术背景久滴的统计架构是T1 落库 今日实时计数双层模型昨日及以前由tradeStatisticsJob每日 00:30 聚合trade_order等表写入trade_statistics看板读落库表毫秒级返回今日由 Redis 实时计数器承担见系列文章 01。这一模型曾暴露三个 P0 问题重复行product_statistics实测存在 16 组(tenant_id, time)重复数据——应用层 check-then-act 防重在多实例部署/Job 重跑下失效趋势缺日GROUP BY DATE(pay_time)对无订单日期直接不产出行前端折线图断裂对照错位今日 vs 昨日的对照曲线按date字段匹配两个区间日期本就不重叠匹配永远落空。实现方案1. 唯一键把防重从应用层下沉到 DB 层22-pro-statistics-performance.sql核心变更-- trade_statistics 无历史重复直接添加ALTERTABLEtrade_statisticsADDUNIQUEKEYuk_tenant_time(tenant_id,time);-- product_statistics 先去重保留 id 最小的一条再加唯一键DELETEpFROMproduct_statistics pINNERJOIN(SELECTMIN(id)ASkeep_id,tenant_id,timeFROMproduct_statisticsGROUPBYtenant_id,timeHAVINGCOUNT(*)1)dupONp.tenant_iddup.tenant_idANDp.timedup.timeANDp.iddup.keep_id;ALTERTABLEproduct_statisticsADDUNIQUEKEYuk_tenant_time(tenant_id,time);设计权衡为什么唯一键含 tenant_id 前缀多租户拦截器为每条 SQL 追加tenant_id条件前缀索引/唯一键能精确定位单租户区段为什么允许冲突失败而不做 upsert同一天同一口径的重复统计数值必然一致唯一键冲突即说明 Job 重跑直接失败是最安全的幂等语义先去重再加键的顺序不可颠倒否则 ALTER 会因存量重复报Duplicate entry。2. 补跑接口与天数上限历史数据初始化或 Job 中断后需补跑statisticsTrade(days)支持按天数回溯/** 补跑天数上限防止误传超大参数长时间占用线程 */privatestaticfinalintMAX_STATISTICS_DAYS1095;OverridepublicStringstatisticsTrade(Integerdays){if(daysMAX_STATISTICS_DAYS){thrownewIllegalArgumentException(交易统计补跑天数不能超过 MAX_STATISTICS_DAYS 天);}// 逐日回溯IntStream.rangeClosed(1, days) 对每一天调用单日统计并汇总结果...}上限 1095 天3 年的来源覆盖业务需要的最长回溯窗口同时避免误传days99999让单个请求占用线程数小时拖垮服务。实际补跑经验930 天历史数据通过管理后台手动执行 Job 传参一次完成。3. fillTrendGaps游标补零保证 X 轴连续趋势接口返回前统一过补零函数按天/月游标生成完整日期序列privateListTradeOrderTrendRespVOfillTrendGaps(ListTradeOrderTrendRespVOlist,LocalDateTimebeginTime,LocalDateTimeendTime,booleanbyMonth){// 1. 查询结果转 Mapdate - VO重复日期取第一条兜底MapString,TradeOrderTrendRespVOmaplist.stream().collect(Collectors.toMap(TradeOrderTrendRespVO::getDate,Function.identity(),(a,b)-a));// 2. 按天/月游标从 beginTime 走到 endTime生成完整日期序列// 月粒度 yyyy-MM对齐 GROUP BY DATE_FORMAT(%Y-%m)日粒度 yyyy-MM-ddListStringkeys/* 游标逐日/逐月推进生成的全部日期 */;// 3. 逐 key 查 Map缺失的补零行orderPayCount0, orderPayPrice0returnkeys.stream().map(key-map.getOrDefault(key,newTradeOrderTrendRespVO().setDate(key).setOrderPayCount(0).setOrderPayPrice(0))).collect(Collectors.toList());}要点补零在服务端而非前端前端无法判断缺失与为 0服务端补零后接口契约变为返回区间内连续完整序列(a, b) - a合并策略即使底层查询意外返回重复日期取第一条兜底不抛异常月粒度用yyyy-MM格式与GROUP BY DATE_FORMAT(pay_time, %Y-%m)的输出格式严格对齐type365YEAR时自动切换。4. 对照数据的序号对齐修复隐蔽 Bug对照场景今日 vs 昨日、本周 vs 上周两个区间的日期必然不同原实现按date查找对照点永远返回 null。修复后先对两侧分别fillTrendGaps再按序号对齐当前区间第 N 天对应对照区间第 N 天。补零在此承担了第二职责——它让两个不等长序列长度一致序号对齐才有意义。注意事项加索引/唯一键选业务低峰期执行trade_order是大表DDL 期间会锁表MySQL 8.0 的 instant/online DDL 对加索引较友好但仍建议避开高峰验证 SQLSHOWINDEXFROMtrade_statistics;-- 应见 uk_tenant_timeSELECTtenant_id,time,COUNT(*)cFROMproduct_statisticsGROUPBYtenant_id,timeHAVINGc1;-- 应为空集补跑前先确认口径补跑按当前代码口径重算若口径曾变更历史区间会被新口径覆盖——先在对账 Job 通过后小范围试跑不要手工 INSERT 统计表绕过 Job 插入的行若与后续 Job 冲突会唯一键报错一律通过手动执行 Job 传参补数。关于久滴直播电商系统JiuDi Live-Mall Service— 面向直播电商场景的全栈后端 Pro 版解决方案。久滴直播电商系统 —— 基于芋道框架深度定制的 SaaS 级直播电商解决方案支持多租户、实时统计与腾讯云直播集成。久滴成河直播成商·Drop by drop, live commerce flows.