社区积分流水对不上账,我顺着回调日志追了三天才发现是"并发扣减"在搞鬼

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 129 浏览 0 回复

上周运营妹子甩了张截图给我,说用户A的积分明明扣了120,但社区帖子列表里显示只扣了100,而且积分流水里两条记录时间戳一模一样,差值刚好是那20。我第一反应是前端传参错了,抓包一看没问题;又怀疑是接口被调了两次,查Nginx日志只有一条POST。头大。

后来把那段扣减逻辑单独拎出来看,才发现是经典的并发坑。我那会儿图省事,扣积分走的是"先查余额→算新值→UPDATE"三步,本地压测根本测不出来,线上稍微有个高峰就撞车。两个请求同时读到余额150,一个算成扣120剩30,一个算成扣100剩50,后写的直接把前一条盖了,流水倒是各插各的,于是账就对不上了。

社区那套积分接口我接的是异步回调模式,用户发帖成功后社区服务端推一条消息到我这边,我再去扣积分、写流水。问题就出在这里:回调本身没做幂等,并发场景下同一个帖子的回调如果因为网络抖动重试了,或者用户手快点了两次,我这边的扣减逻辑完全扛不住。

修的时候分了四步走,记一下给同样接社区接口的兄弟参考:

第一步,回调加幂等。 社区回调里有个 `event_id`,我这边建了个 `callback_log` 表,唯一索引锁死。进来先插,插不进去直接返回200,别抛异常,不然社区那边以为你没收到还会重试。

第二步,扣积分上乐观锁。 余额表加个 `version` 字段,UPDATE 的时候 `WHERE version = 旧值`,返回影响行数为0就说明撞车了,抛个特定异常让队列重试。ThinkPHP 里用 `Db::raw` 或者模型里的 `lock(true)` 都能搞,但我嫌悲观锁太堵,还是乐观锁适合这种读多写少的场景。

第三步,流水和余额必须同事务。 之前流水是扣完积分另外插的,中间如果报错了余额回了流水没回,又是一条坏账。现在包在一个事务里,回滚一起滚。TP6 的事务嵌套我测了下,默认支持,但嵌套层数别太多,容易把自己绕进去。

第四步,对接口返回值做兜底校验。 社区回调成功标准是HTTP 200,但我这边扣积分如果内部异常了,之前直接抛500,社区会重试,重试又触发同样的问题。现在改成内部异常先记日志,返回200但带个 `process_status: failed`,再配个定时任务扫补偿。相当于异步变准同步,至少账面不会乱。

改完上线那天晚上我盯着监控看了两小时,看着积分流水和余额终于对上了,比看流量暴涨还踏实。后来复盘的时候想,这种对接第三方社区系统的活,文档里写的"简单POST回调"背后全是坑,尤其是涉及钱的字段,再怎么谨慎都不为过。

另外提一嘴,社区那边积分接口的签名算法文档写的是 `ksort + json_encode`,但没提 `JSON_UNESCAPED_UNICODE`,我一开始用默认参数验签总不过,翻他们SDK源码才发现加了这货。文档和实现不一致的坑,估计不少人都踩过。

你们接社区接口的时候,有没有遇到过回调顺序乱掉的情况?比如先收到"回帖积分奖励",后收到"发帖基础积分",导致业务上判断帖子是否存在时还没入库。这种时序问题我暂时用延迟队列拖了5秒处理,感觉不是长久之计,有更好的招儿欢迎聊聊。

评论0
回复 · 0
还没有回复
微信客服 微信客服