朋友老李在一家做在线教育的公司干技术,前阵子他们平台搞了个答题赚金币的活动。用户答对题目能领金币,攒够了能换课程优惠券。刚开始挺热闹,结果没几天,系统突然大面积封号,好几千用户账号被冻结,客服电话直接被打爆。
表面是风控,根子在架构
一开始大家都以为是有人用脚本刷金币,触发了风控规则。但查了一圈日志发现,问题不在外部攻击,而是系统自己“误伤”。原来他们的金币发放逻辑和用户行为判定完全耦合在一个服务里,每发一次金币,都要同步校验用户是否异常。高并发一来,数据库锁得死死的,响应延迟飙升,部分请求超时后重试,导致同一道题被重复计分,金币多发。系统一看某用户短时间内金币猛增,立马判定为作弊,直接封号。
微服务拆分不是万能药
后来他们想拆微服务,把金币系统独立出来。想法挺好,可实际落地时,两个服务之间还是靠同步接口通信。比如用户答题完成,主业务系统要等金币服务返回成功才确认答题完成。这样一来,一旦金币服务抖一下,整个答题流程就卡住,用户体验直线下降。更麻烦的是,数据一致性全靠接口调用保证,网络波动时,要么丢金币,要么重复发。
用消息队列解耦才是正路
真正解决问题的办法,是引入消息队列做异步解耦。用户答完题,只往消息队列发个事件,比如:
{"event": "question_answered", "user_id": 12345, "question_id": 678, "correct": true}然后由独立的消费者去处理金币发放。这样主流程不用等金币结果,响应快了,系统也稳了。
封号逻辑得有缓冲机制
还有一个坑是封号太“果断”。系统一检测到异常行为,立刻冻结账号,没有二次确认,也没有人工复核通道。正确的做法是先打标记,进入观察期,比如记录用户连续答题时间、操作间隔、设备指纹等,综合评分。超过阈值再预警,而不是直接封。像打游戏连赢几把不会直接被封号一样,得留点容错空间。
数据一致性不能靠猜
金币这类敏感数据,必须保证最终一致。他们后来加了对账任务,每天晚上跑一次,比对用户答题记录和实际到账金币,发现不匹配的自动进异常队列,由运营人工处理。同时在数据库层面加了唯一索引,防止同一次答题重复发币。代码上也加上了幂等控制:
IF NOT EXISTS (SELECT 1 FROM user_coins_log WHERE user_id = 12345 AND source_type = 'answer' AND source_id = 678) THEN INSERT INTO user_coins_log ... END IF;
现在他们平台又上线了新活动,用户抢着参与,再没出现过批量封号的情况。老李说,技术这东西,不怕复杂,就怕图省事,把不该绑在一起的东西硬凑一块儿。