积分变更必须用事务包裹并配合乐观锁,流水表需含trace_id、前后余额等字段,Redis缓存余额并异步双写,过期清理须分批避峰。
用户做签到、下单、评价等行为时,积分变动常伴随其他业务操作(如更新订单状态)。若不加事务,高并发下 SELECT ... FOR UPDATE 缺失或 UPDATE 未隔离,会出现超发或漏扣。比如两个线程同时读取用户当前积分为 100,各自加 10 后写回 110,实际应为 120。
BEGIN TRANSACTION 内完成,且事务粒度尽量小UPDATE user_points SET points = points + ? WHERE user_id = ? AND version = ? 配合乐观锁(version 字段),比悲观锁更轻量SELECT 再 UPDATE 的两步写法;如必须查后改,务必加 SELECT ... FOR UPDATE 且确保走主键或唯一索引REPEATABLE READ,但无法防止“幻读”场景下的计数偏差,积分流水表插入和余额更新需在同一事务中完成单纯记录“+10 分”无法追溯来源,也无法对账或回滚。每条积分变动必须对应一次明确的业务动作,并支持重放与校验。
point_log 表至少包含:id(自增)、user_id、order_no(如订单号/活动ID)、type(枚举:sign_in、order_pay、re
fund、admin_adjust)、amount(正负整数)、before_balance、after_balance、trace_id(全局幂等键)trace_id 必须由上游业务生成并保证唯一(如 order_pay_20250520123456789),插入前用 INSERT ... ON DUPLICATE KEY UPDATE 防重before_balance 和 after_balance —— 它们是核对最终余额是否一致的关键依据,排查问题时比翻日志快得多CREATE TABLE point_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_no VARCHAR(64) DEFAULT '', type TINYINT NOT NULL COMMENT '1=签到,2=支付,3=退款,4=后台调整', amount INT NOT NULL, before_balance INT NOT NULL, after_balance INT NOT NULL, trace_id VARCHAR(128) NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_created (user_id, created_at), INDEX idx_trace (trace_id) ) ENGINE=InnoDB;
用户首页频繁查积分,若每次都查 user_points 表,主库压力大,且一旦慢查询拖垮连接池,整个登录链路都会卡住。
hash:键为 user:points:{user_id},字段为 balance 和 version
全表扫描删过期积分,锁表时间长、IO 压力大,高峰期可能直接拖垮主库。
point_expired_202504),或在流水表加 expired_at 字段并建联合索引 (user_id, expired_at)
DELETE FROM point_log WHERE id ,每次取最大 id 作为下次起点
Rows_affected 和执行耗时;单次超过 2 秒应暂停几秒再继续trace_id 设计和 Redis 回填时的并发竞争——这两处出问题,基本意味着积分对不上,而且很难定位。