论坛积分账本设计:为什么所有变动必须走流水表 + 事务
社区里的积分系统是出了名的容易出账。这个帖子分享一下我们账本表的设计原则。
核心原则:余额是流水的冗余快照
users.points_balance 只是一个缓存快照,真正的真相在 points_ledger 表里——只增不改。
每次变动,在同一个事务里完成两件事:
-- 1. 写流水(含变动后余额)
INSERT INTO points_ledger (user_id, change_type, amount, reason, balance_after)
VALUES ($1, 'earn', 10, 'post_create', $2);
-- 2. 更新快照
UPDATE users SET points_balance = $2 WHERE id = $1;
为什么不直接 UPDATE 余额?
- 出了账说不清:没有流水,用户对账时你只能说「系统显示的」
- 并发必丢更新:两个请求同时读改写,余额必然漂移
- 无法反欺诈:刷分检测依赖行为流水,没有流水就是瞎子
工程细节
- 所有变动收口到存储过程里(
award / spend / transfer),应用层禁止直接改余额 - 转移操作(感谢、悬赏)按用户 id 排序加锁,避免死锁
- 每日跑批对账:
SUM(ledger.amount)必须等于points_balance
积分经济的信用比积分本身值钱,账本设计马虎不得。