积分系统核心是精度、性能与一致性,应使用long存储最小单位积分(如1积分=100小积分),禁用float/double;数据库用BIGINT;网关层统一转换前端浮点输入;单账户变更可用AtomicLong,但“查-判-扣”需CAS或加锁保证原子性。
Java 里做积分统计系统,核心不是“能不能算”,而是“怎么算才不丢精度、不超时、不串账”。浮点数 double 直接存积分余额?上线三天就有人投诉少了 0.01 分——这是最常踩的坑。
long 存“最小单位积分”,别碰 float/double
积分本质是计数型数据,没有小数意义。所谓“0.5 积分”其实是设计缺陷,应统一换算为“1 积分 = 100 小积分”,全部用 long 运算。
addPoints(long delta))只接受 long,拒绝 double 或字符串输入BIGINT,不是 DECIMAL(10,2) 或 FLOAT
150,失败则拒收,不默默截断或四舍五入AtomicLong 足够应付大多数并发更新,但要注意读写分离场景单账户积分变更(如签到+100、兑换-500)用 AtomicLong 是安全且高效的;但如果要“查余额 → 判断是否足够 → 扣减”,这三步不是原子的,必须加锁或改用 CAS 循环。
public boolean tryDeduct(long userId, long cost) {
while (true) {
long current = pointsMap.get(userId).get();
if (current < cost) return false;
if (pointsMap.get(userId).compareAndSet(current, current - cost)) {
return true;
}
// CAS 失败,重试
}
}compareAndSet 可能反复失败,需限制重试次数(如 3 次),避免线程饥饿SELECT ... FOR
UPDATE)AtomicLong 不解决跨 JVM 问题——集群部署时不能只靠它,得依赖数据库或 Redis 的原子操作每天凌晨跑一次 INSERT INTO monthly_rank_202504 SELECT user_id, SUM(points) FROM point_log WHERE log_time BETWEEN '2025-04-01' AND '2025-04-30' GROUP BY user_id ORDER BY SUM(points) DESC LIMIT 100,比每次请求都 GROUP BY 快一个数量级。
point_log_20250401),删旧数据时直接 DROP TABLE,不走 DELETE
ZSET,但更新策略是“写日志 → 异步任务刷榜”,不是每次加积分都 ZINCRBY
SELECT SUM() 全表扫,而是维护一张 user_daily_points 表,按天聚合好最容易被忽略的是“积分过期”逻辑:它不只是删记录,还要补一条“过期清零”流水,确保账务可追溯。哪怕业务说“过期就没了”,审计字段(expire_reason、expire_at)也得写进主表,不然半年后对不上账,没人知道那 2 万分是怎么消失的。