积分为什么要延迟到账:一次对异步结算的实测与拆解
一、四个任务,四种到账速度
同一个页面上,四个任务的完成时间相差不到一分钟,积分到账却横跨了三个数量级:最短 1 分钟,最长接近 2 天。
我把两周里每次提交到余额变化的时间间隔记了下来,得到的分布大致是这样:
| 任务 | 需要核对什么 | 实测到账延迟 |
|---|---|---|
| 每日签到 | 站内记录 | 秒级到 1 分钟 |
| 浏览案例 | 站内停留时长 | 3–5 分钟 |
| 发布文章 | 内容审核 | 1–3 分钟 |
| 提交 Issue | 第三方平台数据 | 约 5 分钟 |
| Star 代码仓 | 第三方平台数据 | 2 小时到数天 |
| 云端实验 | 实验报表 | 次日 |
| 认证类 | 人工发证 | 数天以上 |
这个分布不是随机的,它精确映射了每个任务背后「要核对的东西离系统有多远」:越靠近自己的数据库,越快;越依赖别人,越慢。
二、同步发奖为什么做不到
如果把「完成任务」和「加积分」写在同一个事务里,会遇到三个绕不过去的问题。
第一,外部系统的响应时间不受你控制。 判断一个用户有没有在第三方代码托管平台 Star 了仓库,只能去问那个平台。它可能限流、可能缓存、可能直接超时。把它放进用户的请求链路里,等于让别人的可用性决定你的页面能不能点。
第二,外部系统的抖动会变成重复发奖。 请求超时后用户重试,前端重发,网关重放——同一个动作被处理三次。如果加积分是同步且非幂等的,用户就多拿了三份。
第三,发错之后无法回滚。 同步链路里,积分一旦写进去,用户可能立刻兑换掉了。事后发现判定有误,你面对的是已经花掉的额度。
所以积分从来不该是「完成动作的返回值」,它应该是「动作被确认之后的一次结算」。
三、异步结算的正确形状
观察下来,这套机制实际由四层组成,缺一层就会出问题。
第一层:事件先落库,判定后置。 用户的点击只负责写一条原始事件,接口立刻返回成功。返回的是「已记录」,不是「已结算」。这一步决定了页面永远快。
第二层:结算器按事件回溯判定。 一个后台任务周期性地把未结算事件捞出来,去问外部系统「这件事到底发生没有」,然后决定发不发。这就是为什么 Star 要等两小时、实验要等一天——不是系统慢,是它在等一个可信的答复。
第三层:周期键保证幂等。 每个任务带一个形如「每日 + 日期」或「每周 + ISO 周序号」的键。同一周期内,无论结算器跑多少次、用户点多少次,都只发一次。幂等不靠去重表,靠业务主键,这是唯一可靠的做法。
第四层:状态与余额分离。 页面上展示的「已完成」和账户里的「余额」是两份数据,更新时机不同。它们会短暂不一致,这是设计,不是 bug。
四、这个设计对使用者的实际影响
理解这四层之后,很多看起来像故障的现象就有解释了。
页面显示「已完成」但余额没变,是第四层在起作用:状态先更新,余额后结算。这时候刷新页面不会让钱更快到,只会让你更焦虑。
反过来,余额涨了但某个任务还显示「去完成」,说明事件已经结算、状态还没回写。这种情况更少见,但同样正常。
真正需要报警的只有一种:超过一个完整结算周期还没有到账。日任务给一天,周任务给一周。在窗口内反复重试,除了制造重复事件,什么也改变不了。
五、如果自己实现,注意什么
结算周期要写进对用户的承诺里。 不要写「完成即到账」,写「T+1 结算」。承诺一个准确的慢,比承诺一个不准的快好得多,因为前者只需要一次解释,后者需要每天解释。
幂等键必须来自业务语义,不能来自请求。 用「用户 + 任务 + 周期」当键,不要用请求 id、时间戳或自增号。前者的重试天然收敛,后者每次重试都是新记录。
外部查询要有独立的降级判据。 第三方接口不可用时,结算器应该挂起而不是判负。判负会永久锁死这个周期的奖励,而挂起只是让它晚一点到账。默认值选错了,误伤的是一批人。
六、一句话总结
延迟到账不是系统的缺陷,是系统在替你确认事实。它慢的每一秒,都在等一个它自己不能编造的答案。
- 点赞
- 收藏
- 关注作者
评论(0)