积分系统核心是精度、性能与一致性,应使用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 -
前端传来的 “1.5 积分” 必须在网关层转成
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 的原子操作
聚合统计(如月度 Top100)别实时算,用预计算 + 时间分区表
每天凌晨跑一次 INSERT INTO monthly_rank_202404 SELECT user_id, SUM(points) FROM point_log WHERE log_time BETWEEN '2024-04-01' AND '2024-04-30' GROUP BY user_id ORDER BY SUM(points) DESC LIMIT 100,比每次请求都 GROUP BY 快一个数量级。
立即学习“Java免费学习笔记(深入)”;
- 日志表按天分表(
point_log_20240401),删旧数据时直接DROP TABLE,不走DELETE - 排行榜缓存用 Redis
ZSET,但更新策略是“写日志 → 异步任务刷榜”,不是每次加积分都ZINCRBY - 用户查自己历史积分走势?不要
SELECT SUM()全表扫,而是维护一张user_daily_points表,按天聚合好
最容易被忽略的是“积分过期”逻辑:它不只是删记录,还要补一条“过期清零”流水,确保账务可追溯。哪怕业务说“过期就没了”,审计字段(expire_reason、expire_at)也得写进主表,不然半年后对不上账,没人知道那 2 万分是怎么消失的。










