积分系统需用数据库原子更新或Redis原子命令操作,强制记录含幂等ID的完整流水,查询分场景选DB直查或带TTL的Redis缓存,扣减前校验余额与状态,逆向冲正依赖流水source和幂等ID。

积分增减必须用原子操作,别直接读-改-写
并发场景下,用户积分被多个请求同时修改,SELECT ... FOR UPDATE 或普通 Get + Update 会导致覆盖丢失。Go 本身没有内置“原子计数器”对数据库字段,得靠数据库原生支持或应用层加锁。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 优先用数据库的原子更新:比如 PostgreSQL 的
UPDATE users SET score = score + ? WHERE id = ?,MySQL 同理;避免先查再算再更新 - 如果业务逻辑复杂(如需校验余额是否足够扣减),用
SELECT ... FOR UPDATE配合事务,但注意锁粒度和超时——别在事务里做 HTTP 调用或 sleep - Redis 场景下,直接用
INCRBY或DECRBY操作score:user:123这类 key,天然原子;但要注意 Redis 和 DB 双写一致性问题
积分流水要记全,尤其状态和幂等 ID
光改总分不记明细,等于没做积分系统。一旦出错,无法对账、无法回滚、用户投诉时拿不出凭证。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每笔变更必须插入一条流水记录,字段至少含:
user_id、amount、balance_after、reason、source(如"order_pay")、status("success"/"failed"/"reversed") - 所有外部触发(如支付回调、活动发放)必须带幂等 ID(如
pay_order_id或自定义trace_id),写流水前先查是否已存在,避免重复入账 - 不要把
reason写成“系统赠送”,而要具体到"invite_reward_v2"或"2024_spring_login"——后续统计和排查都靠它
查询总分别扫表,缓存要带失效边界
每次查用户积分都 SELECT score FROM users WHERE id = ? 看似简单,但高并发下会成为 DB 瓶颈;全量缓存又难保一致。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用户中心页等强实时场景,直接查 DB 主库(加索引),别为了省一次查询引入最终一致性风险
- 列表页、排行榜等弱实时场景,用 Redis 缓存,key 设计为
user:score:{id},设置 TTL(如 60s),并配合“写穿透”策略:更新 DB 后立即SET新值并重置 TTL - 禁止用
SCAN或KEYS扫描 Redis 查所有用户积分——这会阻塞实例;排行榜用ZSET,按分数排序查 topN
负分限制和冻结机制得在写入路径上硬控
允许积分负数?允许冻结用户扣分?这些不是配置开关,而是写操作前必须检查的业务规则。漏掉就可能被刷分或绕过风控。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 扣分前必须查当前余额:
SELECT score, status FROM users WHERE id = ?,status 为"frozen"则拒绝;余额不足且不允许负分,则直接返回错误 - 不要只在前端或中间件拦截,所有入口(HTTP API、MQ 消费、定时任务)都得走同一套校验逻辑,封装成
CheckAndDeductScore()这类函数 - 冻结操作本身也要记流水(
reason="admin_freeze",amount=0),否则解冻时无法还原原始状态
最麻烦的不是怎么加减分,是当一笔订单退款、一个活动下线、一个用户注销时,如何安全地逆向冲正所有相关积分——这时候每条流水的 source 和幂等 ID 就成了唯一可信依据。


















