当前位置: 首页 > news >正文

美团二面挂了!问 “用户积分系统怎么设计”,我答 “加个字段存总数”,面试官:积分过期你怎么算?

昨晚一个 4 年经验的粉丝复盘美团到店事业群的面试,心态崩了。

面试官问了一个非常经典的营销场景题:“我们要设计一个用户会员积分系统。用户购物送积分,积分一年后过期。请问怎么维护积分的有效性?怎么实现过期提醒?数据库怎么设计?

这兄弟心想,积分不就是个数字吗?自信地回答:“简单!在用户表里加个points字段。加积分就Update + N,消费就Update - N。至于过期提醒,搞个定时任务每天扫全表,看谁快过期了就发短信。”

面试官听完,冷笑了一声,抛出核弹级追问:

  1. “用户 1 月得了 100 分(明年 1 月过期),6 月得了 50 分(明年 6 月过期)。8 月他消费了 20 分。请问扣的是哪笔积分?剩下多少?过期时间变了吗?

  2. “你有 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 分时:

  1. 查询point_detail_bucket,按expire_time ASC排序。

  2. 找到第一条快过期的记录(1月入账的),current_balance够扣就扣,不够就扣完这条再找下一条(递归扣减)。

  3. 更新user_point_wallet总数。 这就是标准的“账本拆分”逻辑。

三、 过期提醒与清理:怎么处理 5 亿数据?

面试官的第二个杀招:“怎么高效提醒用户积分快过期了?”

错误解法:实时扫描

每天扫数据库WHERE expire_time = 明天死穴:5 亿用户扫不动。而且如果用户今天把积分花光了,你明天还发短信说“你有积分快过期”,用户会觉得你系统有 Bug。

王者解法:离线计算 + 惰性清理

1. 提醒策略(T+1 离线计算)

针对 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 扣减 + 离线提醒’

  1. 数据库设计:我将数据拆分为总额表(读视图)流水表(不可变日志)明细分桶表(核算核心)。利用明细表记录每笔积分的过期时间。

  2. 扣减逻辑:遵循FIFO 原则,优先扣减快过期的记录。针对碎片化积分,我设计了‘定期合并机制’防止数据库锁冲突。

  3. 资产安全:积分作为虚拟资产,核心写操作坚持走 DB 事务,拒绝 Redis 异步写,杜绝资损风险。

  4. 过期提醒:放弃在线扫表,采用‘Hive/Spark T+1 离线计算’方案,并配合MQ 削峰进行推送。如果是中小规模系统,也可以降级为‘日历表’方案,兼顾成本与效率。”

写在最后

积分系统不是简单的计数器,它是资产管理。 面试官考的不是 SQL 怎么写,而是你对“数据一致性”“海量数据处理成本”的把控。账记清楚了,系统就稳了。

https://mp.weixin.qq.com/s/vvSLW4EnsYEyHYIbGl8q2Q

http://www.jsqmd.com/news/330941/

相关文章:

  • C 语言中的结构体
  • Qwen3-VL-0.6B?Reyes轻量化折腾:一个从0到1开始训练的0.6B参数量的多模态大模型
  • 计算机基础·cs336·MoE
  • Docker Desktop 在国内使用的囧境:镜像拉取失败、加速器失效与破局之道
  • UnityNFE(NetcodeForEntities)入门手记
  • 笔记04:价值链深度游:追踪一包纸巾的“数字一生”
  • 交直流混合微网 程序matlab 采用拉丁超立方抽样和多场景缩减,考虑风光等随机性建模,利用粒...
  • P4113 [HEOI2012] 采花 题解
  • 笔记01:当IT系统“雪崩”,没有一片生意雪花是无辜的
  • CSS3 多媒体查询实例
  • 实测微信立减金回收平台,京顺回收高效变现
  • 笔记02:快消公司的赚钱公式:你写的每一行代码,都在利润表上哪个位置?
  • 今日所为
  • Spring 核心原理深度解析:Bean 作用域、生命周期与 Spring Boot 自动配置
  • 宏智树 AI:破解论文降重 + 去 AIGC 痕迹双难题,学术写作不踩坑!
  • Webpack的常用概念和基本配置
  • 测试文件所使用的依赖
  • 位运算---LC371两整数之和
  • 宏智树 AI:把期刊论文写作变成 “按图索骥”,新手也能精准踩中录用要点
  • SSM毕设项目:基于SSM的学生选课管理系统(源码+文档,讲解、调试运行,定制等)
  • Spring Boot 与数据源的集成
  • jQuery Mobile 表单选择
  • 【毕业设计】基于SSM的学生选课管理系统(源码+文档+远程调试,全bao定制等)
  • 宏智树 AI:3 类学术 PPT 零门槛!开题、答辩、汇报 30 分钟搞定
  • Spring Boot 的安全机制
  • 古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
  • 为什么懂开发的UI设计公司更容易成功?
  • jQuery Mobile 按钮:全面解析与最佳实践
  • Python 学习资源汇总手册
  • 【笔记】【筹码分布图】