fix(stats): 今日计数不再被每日缓存冻结,日期分组不依赖库会话时区
自查部署结果时发现两处正确性问题,都是本页上线时引入的: 1. 「今日提问 / 今日回答」原本随整页聚合一整天缓存,页面却写着「实时」, 实际当天第一次访问后数字就冻住到次日。现拆出 TodayCounts 单独取, 60 秒 TTL、跨日按日期失效;命中整页缓存时返回副本,不原地改缓存指针 (那会被并发读踩成数据竞争)。 2. 日/月/时分组用 to_char/EXTRACT 直接作用在 timestamptz 上,结果跟着数据库 会话时区走,而 PG 官方镜像默认 UTC、Go 侧按容器时区(Asia/Shanghai)切今天。 实测同一条 2026-10-24 01:30+08 的提问,UTC 会话下被算成 10-23 17 点 —— 日报表整体偏 8 小时、热力图时段错位,且与「今日」对不上。 现统一经 analyticsLocalize() 加 AT TIME ZONE INTERVAL 换算,偏移取自 time.Time.Zone()(Go 未设 TZ 时给 "Local",PG 不认该名,故不用 IANA 名)。 过滤条件仍是裸列,索引照用。 另外两处顺手修: - PeakDay 用 >= 比较,并列时会漂到最近的同值日期,与 busiestSlot 的取值规则不一致。 - 聚合改用 context.WithoutCancel + 30s 超时:第一个访客在聚合途中关掉页面时, 排在后面等锁的请求不会各自重跑一遍全表 GROUP BY。 测试:新增 TestTodayNumbersNotFrozen(缓存命中后今日数仍更新、整页聚合当天只算一次) 与 TestTodayCacheExpiresAcrossMidnight;analytics 连真库通过。
This commit is contained in:
@@ -33,7 +33,40 @@ db.Analytics.SiteAnalytics(ctx, now) // 纯 SQL 聚合,无缓存
|
||||
- 回答耗时用 `updated_at - created_at`,但**置顶等写操作也会刷新 updated_at**,
|
||||
所以只对外给「24 小时内占比」这种粗结论,不宣称中位数。
|
||||
- 环比是「上月 vs 上上月」,不是「本月 vs 上月」:本月没过完,每月 1 号会得出 -95%。
|
||||
- 累计与图表是当日 0 点快照,「今日提问/回答」两项实时。页面底部已写明。
|
||||
- 累计与图表是当日 0 点快照,「今日提问/回答」两项实时(60 秒 TTL,见下)。
|
||||
- 日/周/时分组必须在 SQL 里 `AT TIME ZONE INTERVAL '+08:00'` 显式换算,
|
||||
不能依赖数据库会话时区 —— 见「时区」一节。
|
||||
|
||||
## 两个已踩过的坑(2026-10-24 部署前自查发现)
|
||||
|
||||
**1. 「今日」两项曾被每日缓存冻住。** 原先 `SiteAnalytics` 里查了今日数,塞进
|
||||
`Overview`,而 `Overview` 整天复用 → 页面顶着「实时」的说明显示一整天不变的常量。
|
||||
现在拆出 `SiteAnalyticsStore.TodayCounts`,`stats` 层单独 60 秒缓存(`todayTTL`),
|
||||
跨日按日期字符串失效(`todayDate`)。缓存命中时返回的是**副本**(`withToday`),
|
||||
不能原地改 `ovCached` —— 那是数据竞争。
|
||||
|
||||
**2. 日期分组默认跟着 PG 会话时区走,通常是 UTC。** PG 官方镜像 `timezone=UTC`,
|
||||
而 Go 按容器本地时区(Dockerfile 设 Asia/Shanghai)切「今天」。实测同一条
|
||||
`2026-10-24 01:30+08` 的提问:会话 UTC 下 `to_char` 给 `2026-10-23`、`EXTRACT(HOUR)` 给 17。
|
||||
→ 日报表整体偏 8 小时(中国 0–8 点被算进前一天),热力图时段全错位,且与「今日」对不上。
|
||||
现在 `analyticsLocalize()` 统一包一层 `AT TIME ZONE INTERVAL '+08:00'`,偏移取自
|
||||
`time.Time.Zone()`(不用 IANA 名:Go 在未设 TZ 时给 "Local",PG 不认)。
|
||||
过滤条件仍是裸列,索引照用(EXPLAIN 确认走 Bitmap Index Scan)。
|
||||
|
||||
## 性能实测(20 万行 questions,本地 PG,冷缓存)
|
||||
|
||||
| 查询 | 耗时 | 扫描方式 |
|
||||
|---|---|---|
|
||||
| totals(全表条件聚合) | 337 ms | Seq Scan |
|
||||
| 字数分布(全表) | 313 ms | Parallel Seq Scan |
|
||||
| 热力图 180 天 | 100 ms | Bitmap Index Scan |
|
||||
| 日趋势 30 天 | 47 ms | Bitmap Index Scan |
|
||||
|
||||
合计约 0.8 秒;1C1G 云机按 2–3 倍算约 2 秒。靠启动后 3 秒预热挪出请求路径,
|
||||
可接受。**若 questions 到百万行**,三次全表扫描会到 6–10 秒,那时应把
|
||||
totals + 字数分布 + 回答耗时合并成一条 SQL(都是同一张表的无条件 CASE 聚合,
|
||||
合并后只扫一次)。现在不做:不知道真实表规模,每天只算一次,不值得为省 0.5 秒
|
||||
把 SQL 拆成大块。
|
||||
|
||||
## 图表实现(零前端库)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user