perf(vote): 结果聚合改 stale-while-revalidate,消除持续投票时的 O(N²)
多选 / 评分 / 排序 / 表单的统计必须在 Go 层逐条 json.Unmarshal 全部提交记录 (ListByVoteID 传 nil 不设 LIMIT),而 Submit 每来一票就 InvalidateResultsCache 删掉缓存 —— 于是第 k 票触发一次 O(k) 重算,持续有人投票时稳态是 O(N²)。 一万票的投票在投票期间每次刷新都要扫一万行 JSON。 现在: - LoadResults 用 GetWithExpiry 读到「已过期但仍在」的旧值,先返回给调用方, 再在后台按投票去重重算(sync.Map 占位 + shared.SafeGo,60s 超时) - 后台只拷贝 ID/Type/AllowCustom 三个字段,不把 handler 正在用的 *db.Vote 带进 goroutine - Submit 从 InvalidateResultsCache(删缓存)改为 ExpireResultsCache(标记过期保留旧值), 否则每次提交都会让下一次读退化成同步全量重算 - 选项变更 / 删除投票仍走 InvalidateResultsCache(结构性变化,旧值已无意义) - TTLCache 新增 GetWithExpiry / Expire:Get 遇到过期项直接删除并返回 false, 「过期」与「不存在」分不开,正是无法做 SWR 的原因 实测(2 万条多选提交):冷启动 0.211s → 过期首读 0.023s(快 9.2x), 后台刷新后 0.009s。缓存单测 3 项覆盖 GetWithExpiry 不删条目 / Expire 保留值。 顺带把 7 处裸类型断言改成带 ok(其中 results.go 的是新代码)。
This commit is contained in:
+11
-2
@@ -1167,9 +1167,18 @@ func VoteResults(ctx context.Context) {
|
||||
formFields, _ = db.VoteFormFields.ListByVoteID(ctx.Request().Context(), vote.ID)
|
||||
// 短 TTL 缓存回复明细解析结果(逐条 json.Unmarshal 全部提交记录的 O(N) 开销)。
|
||||
replyKey := strconv.FormatUint(uint64(vote.ID), 10)
|
||||
// 带 ok 的断言:缓存里若混入别的类型,裸断言会直接 panic 掉整个结果页。
|
||||
// 类型不符时丢弃该条缓存并走重建分支(所以不能用 else 接在外层 if 后)。
|
||||
fromCache := false
|
||||
if cached, ok := voteTextRepliesCache.Get(replyKey); ok {
|
||||
textReplies = cached.(map[string][]textReplyView)
|
||||
} else {
|
||||
if parsed, isMap := cached.(map[string][]textReplyView); isMap {
|
||||
textReplies = parsed
|
||||
fromCache = true
|
||||
} else {
|
||||
voteTextRepliesCache.Delete(replyKey)
|
||||
}
|
||||
}
|
||||
if !fromCache {
|
||||
submissions, _ := db.VoteSubmissions.ListByVoteID(ctx.Request().Context(), vote.ID, nil)
|
||||
for _, s := range submissions {
|
||||
var data map[string]interface{}
|
||||
|
||||
Reference in New Issue
Block a user