昨晚一个 4 年经验的粉丝复盘美团到店事业群的面试心态崩了。面试官问了一个非常经典的营销场景题“我们要设计一个用户会员积分系统。用户购物送积分积分一年后过期。请问怎么维护积分的有效性怎么实现过期提醒数据库怎么设计”这兄弟心想积分不就是个数字吗自信地回答“简单在用户表里加个points字段。加积分就Update N消费就Update - N。至于过期提醒搞个定时任务每天扫全表看谁快过期了就发短信。”面试官听完冷笑了一声抛出核弹级追问“用户 1 月得了 100 分明年 1 月过期6 月得了 50 分明年 6 月过期。8 月他消费了 20 分。请问扣的是哪笔积分剩下多少过期时间变了吗”“你有 5 亿用户你打算每天全表扫描一次数据库来发提醒就算你扫得动万一用户消费了积分你提醒的数据还没更新用户投诉你诈骗怎么办” 这哥们瞬间哑口无言才意识到自己把“资产管理系统”想成了“计数器”。兄弟们积分系统本质上是一个“准金融系统”。 它涉及流水追溯、效期管理、对账核算。单纯存一个总数是绝对不够的。今天 Fox 带你拆解这道题的3 种段位看看大厂是如何管理这笔“虚拟资产”的。一、 为什么 “单字段存总数” 是自杀行为如果你只在 User 表存一个total_points你就丢失了积分的“时间维度”。场景还原入账2023-01-01 获得 100 分有效期 1 年。入账2023-06-01 获得 50 分有效期 1 年。消费2023-08-01 消耗 20 分。问题来了这 20 分扣的是 1 月的还是 6 月的 根据“对用户最有利原则”系统必须遵循FIFO先进先出逻辑优先扣除“快过期”的积分。 如果你只有一个total_points 130你根本不知道每笔积分的“生死簿”到了 2024-01-01你该让多少积分过期根本算不出来。结论积分系统必须采用“分桶存储”策略而不是简单的总量存储。二、 核心架构数据库设计要解决过期和扣减问题数据库设计至少需要3 张核心表。1. 积分总表User_Point_Wallet作用相当于“钱包”只存总额用于快速读取展示给用户看。CREATE TABLE user_point_wallet ( user_id BIGINT PRIMARY KEY, total_balance INT DEFAULT 0, -- 当前可用总积分 version INT DEFAULT 0, -- 乐观锁版本号 update_time DATETIME );2. 积分流水表Point_Flow_Log作用相当于“银行流水”记录每一笔增减操作不可修改用于对账。CREATE TABLE point_flow_log ( flow_id BIGINT PRIMARY KEY, user_id BIGINT, amount INT, -- 变动金额100 或 -20 type TINYINT, -- 类型1-签到2-购物3-兑换4-过期 ref_id VARCHAR(64), -- 关联业务单号 create_time DATETIME );3. 积分明细/分桶表Point_Detail_Bucket作用记录每一笔入账积分的余额和过期时间。CREATE TABLE point_detail_bucket ( id BIGINT PRIMARY KEY, user_id BIGINT, initial_amount INT, -- 初始入账金额如 100 current_balance INT, -- 当前剩余金额初始 100消费后变 80 expire_time DATETIME, -- 过期时间决定了它的生死 status TINYINT, -- 0-有效1-已用完2-已过期 INDEX idx_user_expire (user_id, expire_time) -- 关键索引按过期时间排序 );消费时基于这张表做 FIFO 扣减。Fox 点评当用户消费 20 分时查询point_detail_bucket按expire_time ASC排序。找到第一条快过期的记录1月入账的current_balance够扣就扣不够就扣完这条再找下一条递归扣减。更新user_point_wallet总数。 这就是标准的“账本拆分”逻辑。三、 过期提醒与清理怎么处理 5 亿数据面试官的第二个杀招“怎么高效提醒用户积分快过期了”错误解法实时扫描每天扫数据库WHERE expire_time 明天。死穴5 亿用户扫不动。而且如果用户今天把积分花光了你明天还发短信说“你有积分快过期”用户会觉得你系统有 Bug。王者解法离线计算 惰性清理1. 提醒策略T1 离线计算针对 5 亿这种体量别做实时提醒成本太高且没必要。方案利用大数据平台Hive/Spark。逻辑每天凌晨把point_detail_bucket的快照同步到数仓。在数仓里跑一个 SQL算出“未来 7 天过期的 Bucket 总额 0”的用户清单。触达MQ 削峰拿到清单后千万别直接调短信接口5 亿用户哪怕只有 1% 过期也是 500 万条短信。必须引入MQ 进行削峰填谷控制发送速率如 5000 QPS避免早高峰把短信网关打挂。备选方案轻量级如果公司没有大数据基建可以退回到‘过期日历表’方案。建一张表按“过期日期”聚合用户 ID每天扫描。虽然维护成本高一点但胜在不需要维护 Hadoop 集群适合中小体量。2. 清理策略惰性 定时不要每天去 DB 里执行UPDATE point_detail_bucket SET status 过期。读时触发用户查积分时前端根据 expire_time 过滤掉已过期的展示“有效余额”。写时触发用户消费时后端过滤掉过期的 Bucket只扣有效的。物理归档针对长期不活跃的“垃圾数据”后台低频 Job 慢慢搬运到历史冷库。四、 最后的“防杠”指南设计完架构面试官一定会进行地狱级追问这 3 个问题答不好前面全白搭Q1如果我每天签到送 1 积分连续签了 3 年我有 1000 条 Bucket。现在我要买一个 500 分的东西岂不是要在一个事务里 Update 500 行记录数据库不就死锁了答“这是个好问题碎片化账本。针对这种‘碎片积分’必须引入‘定期合并Compaction’机制。 后台 Job 会定期扫描 Bucket 表把同一个用户、相同过期时间或者相近过期时间的多个小 Bucket合并成一个大 Bucket。 这样扣减时最多操作几行记录性能就保住了。”Q2为了高性能我能不能先把积分写入 Redis异步落库答“绝对不行积分是资产资产数据要求强一致性。Redis 是弱一致的万一 Redis 挂了或者 MQ 丢消息用户的钱就没了或者变多了这是 P0 级资损。正确做法核心扣减必须走MySQL 事务。Redis 只能用来做‘读缓存’Cache Aside加速查询绝不能作为数据的‘源头’。”Q3用户退款了积分怎么退有效期怎么算答“原路退回原则。扣减时我们会记录一条consumption_log消费明细记录了这笔订单具体扣了哪些 Bucket、各扣了多少。 退款时根据日志逆向恢复。如果该 Bucket 还没过期就恢复余额。如果该 Bucket 已经过期了通常策略是不退或者退回并延期 7 天看业务良心。”五、 面试标准答案模板建议背诵下次被问到“积分系统设计”直接按这个套路输出“对于积分系统我的核心设计思路是‘总分分离 FIFO 扣减 离线提醒’数据库设计我将数据拆分为总额表读视图、流水表不可变日志和明细分桶表核算核心。利用明细表记录每笔积分的过期时间。扣减逻辑遵循FIFO 原则优先扣减快过期的记录。针对碎片化积分我设计了‘定期合并机制’防止数据库锁冲突。资产安全积分作为虚拟资产核心写操作坚持走 DB 事务拒绝 Redis 异步写杜绝资损风险。过期提醒放弃在线扫表采用‘Hive/Spark T1 离线计算’方案并配合MQ 削峰进行推送。如果是中小规模系统也可以降级为‘日历表’方案兼顾成本与效率。”写在最后积分系统不是简单的计数器它是资产管理。 面试官考的不是 SQL 怎么写而是你对“数据一致性”和“海量数据处理成本”的把控。账记清楚了系统就稳了。https://mp.weixin.qq.com/s/vvSLW4EnsYEyHYIbGl8q2Q