导读:积分体系的噩梦是"账对不上"——用户说积分少了,运营说发对了,技术查了半天发现是并发扣减写脏了。积分本质是账户余额系统,核心三件事:账户怎么存、流水怎么记、并发怎么扣。本文给出可直接落地的积分账户设计,含流水幂等、余额并发扣减与对账方案。
积分系统的第一原则是"余额不直接改,流水说了算"。设计两张表:账户表存当前余额(可缓存),流水表记录每一笔变动(只追加)。余额只是流水的聚合视图,对不上账时以流水重建余额。
CREATE TABLE point_account (
user_id BIGINT PRIMARY KEY,
balance INT NOT NULL DEFAULT 0,
frozen INT NOT NULL DEFAULT 0,
updated_at DATETIME
);
CREATE TABLE point_flow (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
change_amount INT NOT NULL, -- 正为加、负为扣
flow_type VARCHAR(32) NOT NULL, -- sign_in/consume/refund/expire
biz_id VARCHAR(64) NOT NULL, -- 业务单号,幂等键
created_at DATETIME,
UNIQUE KEY uk_biz (user_id, flow_type, biz_id)
);要点:biz_id 是幂等键——同一业务单(同一笔下单、同一场签到)只允许产生一条流水,防止接口重试导致重复加积分。
积分扣减最容易翻车的写法是"先 SELECT 余额 → 判断够不够 → UPDATE"。并发下两个请求同时读到余额 100,各扣 80,都以为成功,实际余额变成 -60。正确做法是条件更新,让数据库保证原子性:
UPDATE point_account
SET balance = balance - 80, updated_at = NOW()
WHERE user_id = ? AND balance >= 80;
-- 受影响行数为 0 表示余额不足要点:一次 UPDATE 同时完成"判断 + 扣减",并发安全;扣减后插入流水,流水写入失败要回滚余额(同一事务)。
下单使用积分时,别立刻扣减,先"冻结":把积分从可用余额转到冻结额,订单完成后解冻扣减、订单取消后解冻退回。这样能防止"下单时够、结算时不够"的纠纷:
-- 冻结:balance 减、frozen 加(原子)
UPDATE point_account SET balance = balance - 500, frozen = frozen + 500
WHERE user_id = ? AND balance >= 500;要点:冻结要有超时释放(比如 30 分钟未支付自动解冻),防止积分被僵尸订单占着。
积分接口要天然支持重试。做法:扣减/增加前先查流水表是否已有该 biz_id,有则直接返回原结果;没有则在同一事务里写流水 + 改余额。流水唯一键就是上文的 UNIQUE KEY uk_biz,数据库层面兜底防重。
再好的设计也怕漏网之鱼,定期对账必不可少:每日凌晨跑一遍"流水汇总 = 账户余额"的核对任务,发现差异按用户维度告警。对账脚本以余额快照与流水时间窗比对,只读不写、跑在从库,避免影响线上。
积分账户要按「账户 → 流水 → 冻结 → 对账」四层设计,余额与流水分离、并发扣减靠条件更新、幂等靠业务单号、一致性靠每日对账。同类分层在乔拓云商城的会员账户模块中有对应实现,中小商家可直接参照该模块的流水与对账策略起步。
积分系统的本质是账本系统:余额是流水的投影,并发靠数据库原子性,一致靠对账兜底。把账户、流水、冻结、对账四件事做扎实,积分就再也不会"对不上"了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。