面试_场景方案设计_联系
1. 电商秒杀1000个手机
我的回答:
好的,首先从架构设计上,这是个秒杀场景,对服务的性能要求极高,所以可以独立出秒杀服务, 专门用于库存的扣减。 结合电商原有的服务, 主要的几个服务模块包括:秒杀网关服务、秒杀服务、库存服务、订单服务、支付服务。秒杀网关服务统一接入秒杀服务流量,用于用户鉴权、流量限流等。
技术的选型上,使用Nocas作为服务注册中心, 服务之间异步通信可以使用rocketmq消息, 缓存使用redis, 数据库采用mysql。
秒杀场景,核心技术点:
1. 库存扣减,保证不能超卖, 又要支持高并发流量。
使用Redis存储库存量, 选择string类型结构,key: goods:phone , value初始值1000, 扣减库存时,先从redis扣减,然后异步的方式通知库存服务扣减mysql中的库存, redis在扣减库存时,首先校验库存是否足够,足够才能扣减,使用lua脚本保证redis扣减库存的原子性。
Redis的高可用高性能保证,Redis扣减库存是核心,要保证redis性能, 使用Redis主从架构,做读写分离, 主节点服务库存扣减,从节点提供读服务。
为了节省库存扣减和查询的速度,可以将Redis库存服务节点和秒杀服务节点部署在一起,减少扩机房的开销。
2. 订单的防重复提交
用户提前订单前提前生成号订单号,在用户下单时带着订单号,利用订单号做好幂等控制。 订单号的生成可以使用JVM缓存,预先+定时+ 少于百分比自动触发生成新的订单号集合的方式。
3. 流量控制
秒杀场景对应非常大的瞬时流量,通过层层限流方式,降低对系统的冲击,比如下订单时增加验证码、黑名单的限制、接口的限流等维度控制流量
4. 静态页面的前置
秒杀场景流量比较大的页面和接口是 秒杀商品详情页, 秒杀商品下单页, 库存扣减, 对页面中的静态信息,前端做好缓存。
5. 订单数据,用户查看自己的订单这个操作非常频繁,而且订单量非常大。可以按照用户uid做分库分表设计。 对超过3个月的数据做数据归档。
同时上线前做好充足的压测,监控。
追问环节
分布式事务的替代方案
你提到了基于MQ的最终一致性。这是一个经典方案,但如果业务方要求更强的一致性(比如,要求Redis扣减和MySQL扣减作为一个事务),除了引入Seata这类重型框架,你是否了解其他更轻量、性能更好的柔性事务方案来替代你当前的设计?
首先,需要是99.9%的秒杀场景,基于MQ的最终一致性已经是成本和收益权衡下的最佳方案。面试官的这个要求,主要还是考察你的思维设计能力。
回答:基于Redis的预扣库存+异步落地
核心思想: 将库存扣减这个最核心的操作,完全收拢在Redis内部完成,并将其视为唯一可信源,后续操作全部基于此结果进行异步化。(单一数据源)
具体流程:
预扣库存(唯一写操作):在Redis Lua脚本中,完成库存校验与扣减。这一步是原子的、强一致的。成功后,我们认为用户已经获得了购买资格。
生成流水凭证:Lua脚本在扣减成功后,生成一个唯一的
token
(或订单ID),并将其与用户ID、商品ID一起写入Redis(设置较短过期时间)。这个token
是后续操作的“门票”。异步创建订单:秒杀服务发送一条延时消息到MQ,内容包含
token
。订单服务消费此消息,需要拿着token
去Redis中查询验证,验证通过才创建数据库订单。(这里是创建订单,而不是扣减库存)最终兜底:如果创建订单失败(极小概率),可以通过一个补偿Job,定期扫描这些“有
token
但无订单”的数据,将其对应的库存回滚(加回Redis),并通知用户失败。
说明:这里就没有库存表的扣减了, redis是库存的唯一数据源。
这个方案的‘强一致’体现在:
对于用户而言,只要收到成功响应,就确定占住了库存,体验上是强一致的。
对于系统而言,库存的最终正确性由Redis和补偿机制保证。
它避免了强一致分布式事务的性能不佳问题。
降级与熔断
假设你的Redis集群出现了网络分区,或者某个核心交换机故障,导致秒杀服务无法访问Redis。在这种情况下,你的整个秒杀流程就中断了。作为一个高可用系统,你的降级方案是什么?如何让系统在部分组件故障时仍能提供有损服务,而不是完全崩溃?
回答:
在Redis故障时,立即在网关头切断绝大部分流量,并直接返回“活动太火爆”等友好提示,同时将少量请求(如1%的流量)引导至一个基于数据库悲观锁的、性能低下但能保证一致性的备用扣减路径,以维持核心业务流程不中断(尽管服务能力大幅下降)。
架构权衡
在你的设计中,用户请求需要经过秒杀网关 -> 秒杀服务 -> Redis 这几个关键路径才能完成库存扣减。在百万QPS的流量下,这个链路的延迟和资源消耗是可观的。你有没有考虑过更极致的架构,比如将秒杀资格校验和库存扣减的逻辑,直接下沉到网关层,利用Lua脚本在网关层面完成,从而减少一次网络跳转?这样做会带来什么好处和弊端?
回答:
好处是显而易见的:减少一次网络跳转(秒杀服务调用Redis),大幅降低请求延迟,并且能在最外层快速拦截无效请求,减轻后端集群的整体压力。
但弊端也同样突出,正如您所指出的:
职责混乱:网关(如Spring Cloud Gateway, Nginx)本应专注于流量治理、路由、鉴权,现在混杂了核心业务逻辑,变得臃肿且难以维护。
技术栈限制:在网关层写复杂的Lua脚本(如OpenResty)或Java代码,其调试、测试、发布流程都比常规微服务要复杂。
数据一致性风险:如果网关处理完扣减后,在调用下游服务时失败,确实需要一套机制来回滚或同步状态。”
更推荐在网关层使用本地缓存做预校验
在网关层进行无损的、只读的预校验,在获得了绝大部分性能收益的同时,完美地规避了数据一致性和架构混乱的风险,是实践中更优的选择。”
这是一个风险更低、收益也很高的方案。我们不在网关头进行写操作(库存扣减),而是只进行读操作。
具体做法:在网关层部署一个本地缓存(如Caffeine, Guava Cache),通过发布订阅机制(如Redis Pub/Sub)或定时任务,从Redis中同步全局库存余量。
当用户请求到达网关时,网关首先查询本地缓存中的库存数量。如果库存已为0,直接返回秒杀结束,无需将任何流量打到后端服务。
这个方案能拦截掉99%以上的无效请求(秒杀开始后不久库存就会告罄),对后端实现了完美的保护。而真正的库存扣减,仍然放在后端的秒杀服务中,通过Redis Lua脚本完成,保证了业务逻辑的集中和原子性。
2、内容社交APP
假设你是一家快速成长型互联网公司的技术负责人,公司的主打产品是一个内容社交APP(类似小红书或微博)。目前,公司决定将原有的 “用户关注关系”模块进行彻底的微服务化改造和架构升级。
现有问题与挑战:
单体架构瓶颈:目前关注关系(如关注、取消关注、查询关注列表)与核心业务逻辑耦合在同一个巨型单体应用中,数据库压力大,迭代困难。
读请求爆炸:核心接口“获取用户关注列表”的QPS随着用户量增长极快,在热门用户场景下,存在严重的热点读问题。
写扩散的困扰:信息流(Feed流)目前采用写扩散(即用户发布内容后,主动推送到所有粉丝的收件箱)。这导致一些大V用户发布内容时,会引发海量的数据库写入,系统不堪重负。
数据一致性:在关注/取消关注、更新用户信息等操作时,需要保证关注关系、粉丝数、Feed流等多个数据源之间的状态一致。
你的任务是:
请设计一个高可用、可扩展的关注关系系统架构,并重点阐述你将如何解决上述挑战,特别是读热点、写扩散以及数据一致性问题。
追问:读请求爆炸 含义:
1. 随着用户总量和用户平均关注数的增长,整个“查询关注列表”接口的总QPS会飞速上升
2. 当数百万甚至上千万用户都关注了同一个或少数几个“大V”用户时会发生什么?
热点数据:这个“大V”用户的关注关系数据就成为了热点数据。具体来说,就是存储
user_id = 大V_ID
的这条“他的粉丝列表”记录。热点读:每当有一个用户访问这个大V的主页,或刷新信息流需要判断“我是否关注了他”时,系统都需要去查询这条“大V的粉丝列表”数据。海量的用户行为会导致在短时间内,海量的读请求都集中指向了这一条(或少数几条)数据。
我的第一轮回答:
(我认为这里的重点是 数据存储设计、关键业务能力描述其实现方案)
1. 用户关注关系”模块独立成一个微服务
2. 技术选型:服务注册中心Nocas, 服务注册与发现, 数据库分库分表Mysql, 缓存Redis
3. 数据表的设计
- “我的关注”表,主要字段有userId、attentionUserId, status, 按照userId分库分表存储;
- “我的粉丝”表, 主要字段有userid、fensiId, status, 按照userId分库分表存储;
- “我的收件箱”表,主要字段userId, attentionUserId, contentId, 按照userId分库分表存储.
- “我的内容”表,主要字段contentId, userId, content, 按照userId分库分表存储.
4. 缓存设计
- 粉丝数使用Redis缓存存储, 提升查询粉丝数的性能, 采用string结构, key=fensi:num:userId value是具体的粉丝数量。
- 登录的用户使用Redis缓存存储。 采用set结构, key=login:user value是每个用户的userId(后面会抛弃掉这个设计)
- 用户登录状态缓存,后面详细设计
- 我的关注 缓存,后面详细设计
5. 数据归档
6. 关键业务功能
- 解决写扩散的困扰, 一些大V用户发布内容时,只将数据写入登录用户的所有粉丝的收件箱中, 极大的减少了写入量。其中登录用户可以从缓存login:user获取, 利用缓存节省查询时间。 当粉丝登录时,在主动去查询关注用户发布的内容
- 关注和取消关注操作, “我的关注”表和“我的粉丝”表使用事务保证数据一致性;
- 粉丝数量, 当有人关注我时,也就是“我的粉丝”表有变更时,删除对应的缓存key(fensi:num:userId), 重新查询我的粉丝数时, 统计数据表“我的粉丝”中我的粉丝数,并重新写入缓存。 这样的目的是为了保持数据一致性
- 我登录后,查看我关注人的信息: 系统提供个大V的判断规则, 对于非大V, 直接读取“我的收件箱”, 对于大V用户, 实时查询某个时间点之后的大V发布的数据
- 系统提供个大V的判断规则
第一轮追问:核心逻辑与一致性
写扩散的可行性:您提到“大V用户发布内容时,只将数据写入登录用户的所有粉丝的收件箱中”。这里的关键词是“登录用户”。我的理解是,您想只给当前在线的粉丝推送内容。但这里有一个核心问题:大V发布内容时,如何实时、高效地获取到他所有“当前在线”的粉丝列表? 这个查询操作本身(例如,从
login:user
这个Set中判断哪些用户既是他的粉丝又在线)在粉丝量达到千万级别时,是否会成为一个新的、甚至更严重的性能瓶颈?
回答:
1. Redis存储所有的登录用户,这个缓存确实是个非常大的集合,违背了避免大key的原则,且在海量数据下性能很差。一个更轻量级的管理登录用户态的做法是: 建立“用户在线状态”的映射:当用户登录时,在Redis中以其 user_id
为Key,记录一个轻量级的在线状态(例如:online_status:{user_id} = 1
,并设置一个短的过期时间,如5分钟,通过心跳续期)。这避免了操作一个巨大的Set。
2. 大V发布内容后,只需将内容ID、大V的用户id 写入一个待分发的消息队列中。
3. 异步逻辑中,采用 “仅推送给在线粉丝”的轻量级写扩散。消费者通过一个服务,获取当前在线的用户ID范围(这可以通过扫描 online_status:*
的Key模式来实现,但更高效的是有专门的在线状态服务)。然后,对于每一个在线用户,去查询一次 “我的关注”关系缓存(这个查询Key是 {online_user_id}_follows_{bigv_id}
,性能极高),判断他是否关注了此大V。如果是,则将内容ID写入该在线用户的收件箱。
2. “我的收件箱”数据一致性问题:在“关注”操作时,是否需要将所关注用户的历史内容同步到我的收件箱?如果需要,如何保证在关注动作完成时,我能立刻看到他的内容,而不出现数据缺失?如果网络中断导致同步失败,如何补偿?
回答:在关注操作时,不应该同步将关注用户的历史内容同步到我的收件箱, 这是为了提高”关注“接口的性能,可以在关注的时候触发一个异步任务【重点是保证这个异步任务的可靠性和数据完整性】,将关主人的历史内容同步到我的收件箱,同步的时候可以只同步某个时间点后(比如1年内)的内容。
升华回答:
- 使用事务消息保证本地的"关注"写操作和发消息的一致性
- 消费端收到消息后,同步关注人某个时间点后的发布的内容, 可以记录一个同步状态(如
sync_status: {follower_id}_{followee_id}
)。如果同步失败,可以通过重试机制继续,实现断点续传。 - 用户体验优化:在异步同步完成之前,如果用户立刻刷新,可能会看不到新关注用户的内容。为了解决这个问题,可以在“关注”API成功返回后,在前端/客户端将该用户的最新几条内容预加载下来,并临时插入到信息流中,从而实现“准实时”的体验。
3. 缓存策略的优化:您提到更新粉丝数时,采用“删除缓存”的策略。在高并发场景下,这可能导致一个经典问题:在缓存失效后、数据库查询完成前,大量请求同时到达数据库,引发“缓存击穿”。您如何预防这个问题?
回答:缓存失效后, 大量请求同时访问,通过分布式锁,保证只有一个请求可以真正访问数据库, 其他请求等一等,然后再读取缓存中的数据。
升华回答:对应第二轮的追问
第二轮追问:架构权衡与演进
写扩散与读扩散的混合模式:您当前的方案是,大V发布内容时,依然尝试异步地写给在线粉丝的收件箱(这是一种写扩散)。考虑到之前提到的,获取大V的在线粉丝列表本身可能就是一个昂贵操作,且大V的粉丝量可能巨大无比(数千万),这个异步写操作本身仍然可能耗时极长甚至失败。
您是否考虑过,对于粉丝数超过某个阈值(例如10万)的大V,直接切换为读扩散?即,他们发布的内容不写入任何粉丝的收件箱,当粉丝查看信息流时,系统实时地去查询所关注的大V们最新发布的内容,再进行聚合。
如果能接受混合模式,您的系统架构将如何同时支持对普通用户使用写扩散和对大V使用读扩散?关键点在于,一个用户的信息流将由两部分组成,您如何设计和聚合?
回答:混合模式确实是一个非常好的思路。首先系统需要有一个大V的判断规则。如果我是个登录态的用户, 在我刷新页面时, 对我关注的超级大V,需要主动查询大V自我登录后发布的内容, 并将这些内容写入我的收件。 然后将我收件箱中的内容按照时间排序展示; 如果我刚刚登录, 对我关注的大V和超级大V都需要实时查询其发布内容,写入收件箱, 然后将我收件箱中的内容按照时间排序展示。
更技术性的实现“对大V和普通用户区别对待”?一个可行的做法是,在用户的关系表中增加一个user_type
字段(普通用户、大V),在聚合信息流时,查询引擎会根据这个类型决定是从“收件箱”(写扩散)拉取,还是从“内容表”(读扩散)实时拉取。 但是这个字段的取值还是得依靠一套大V判断规则产生。
2. 分布式锁的性能与风险:您提出用分布式锁来解决粉丝数缓存的击穿问题,这是一个标准答案。
但在极致高并发的场景下(例如顶流明星官宣恋情,瞬间海量关注),获取分布式锁本身可能成为瓶颈,大量线程阻塞等待,可能导致线程池耗尽或服务雪崩。
除了分布式锁,您是否了解其他更轻量级、无锁化的方案来解决缓存击穿/重建问题?(提示:可以考虑在逻辑过期时间或提前预热上做文章)
回答:粉丝数,从业务上可以是一个相对没那么精确的值, 所以用用户关注的时候,可以不删除这个缓存, 防止缓存失效访问数据库,可以使用binlog日志, 当”我的粉丝“表有变更时,利用canal组件,将结果同步到缓存。
3. 数据归档与用户体验:您提到了对“我的收件箱”和“我的内容”进行定期归档。
当您归档一个用户三年前发布的某条内容后,如果该用户的一位新粉丝,通过异步任务试图拉取这条历史内容,会发生什么?您的系统如何保证在归档后,这种“关注后同步历史内容”的流程依然能正确工作,而不造成数据丢失或逻辑异常?
回答:建立统一的数据查询路由层,根据查询的时间范围 自动判断是查询在线数据库还是归档库(如HBase、对象存储),对应用层透明,从而系统化地解决此类问题。
总结:
1. 缓存的设计,避免大key, 但是可以多设置缓存的个数,注意过期时间的设置+通过心跳续期
2. 网络中断导致同步失败,如何补偿, 想到用”同步状态“记录最新的同步进度,配合重试机制继续,实现断点续传。
3. 缓存一致性,分布式锁设计时,考考虑到极高的并发量时,获取分布式锁本身可能成为瓶颈,大量线程阻塞等待,可能导致线程池耗尽或服务雪崩。 想到更优的解决方案:1. 永不过期 + 后台更新: binlog触发缓存更新 2. 逻辑过期时间 + 异步刷新 :优化缓存key的设计,缓存Value中,不再只存储粉丝数,而是存储一个封装对象,如:{value: 12345, expire_time: 1741026000}
。其中 expire_time
是一个未来的时间点(即逻辑过期时间)。
4. 对业务中涉及到的通用能力,回答 建立统一的能力,比如建立统一的数据查询路由层、基于Binlog的缓存更新”沉淀为一套通用数据同步平台。
技术细节
“用户在线状态”的映射, 心跳续期
方案一: 基于Redis的轻量级实现(最常见)
用户成功登录后,服务端在Redis中设置一个Key。
Key:
online_status:{user_id}
Value: 可以是一个简单的
1
,或者包含更多信息如登录时间、设备类型的JSON。过期时间(TTL):设置为一个较短的时间,例如 30秒。
SET online_status:12345 1 EX 30
2. 心跳续期
# 命令示例:将Key的过期时间重置为30秒后
EXPIRE online_status:12345 30
# 或者直接重新设置,效果相同
SET online_status:12345 1 EX 30 KEEPTTL
客户端(Web/App)每隔一个固定的时间(例如 每20秒)向服务端发送一个轻量的HTTP请求或WebSocket帧,即“心跳包”。
服务端收到心跳后,执行一个简单的Redis命令:刷新这个Key的过期时间。
用户离线
如果用户正常退出,客户端发送一个“注销”请求,服务端主动删除
online_status:{user_id}
这个Key。如果用户异常离线(断网、App被杀),心跳会停止。30秒后,Redis会自动删除这个Key,系统即认为用户已离线。
方案二:基于WebSocket的长连接
“我的关注”关系缓存
为了实现Feeds流,对”我的粉丝“做缓存,这个数据量可能会很大,所以选择对“我的关注”做缓存。
1. 缓存数据结构
数据结构: String 或 Set(各有适用场景)
Key设计:
方案A(查询单一关系):
follow_status:{follower_id}_{followee_id}
Value:
1
(表示关注) 或直接删除Key (表示未关注/取消关注)。优点:查询速度极快,直接O(1)判断。
缺点:在大V场景下,要判断千万粉丝,需要执行千万次
GET
命令,即使用Pipeline也开销巨大。不适用于您提到的“大V发布时反向判断在线粉丝”的场景。
方案B(查询用户的所有关注):
following_set:{follower_id}
Value: 一个Set,存储该用户所有关注的人的ID (
followee_id
)。优点:对于“获取用户关注列表”这种操作非常高效,一次
SMEMBERS
即可。对于反向判断,使用SISMEMBER following_set:{follower_id} {followee_id}
命令,也能在O(1)时间内完成判断。缺点:如果一个用户关注了很多人,这个Set会很大,占用内存多,
SMEMBERS
全量获取时网络传输量大。
方案C(最优解-布隆过滤器):
following_bf:{follower_id}
Value: 一个布隆过滤器 (Bloom Filter),预先将用户所有关注的人的ID添加进去。
优点:内存占用极小,查询速度极快。
缺点:存在一定的误判率(可能错误地判断“已关注”,但绝不会错误判断“未关注”)。在社交场景中,这个特性是可接受的:误判的唯一后果是可能给一个非粉丝用户推送了内容,但因为他根本看不到大V的私密内容,所以业务上是安全的。这对于“在线粉丝推送”的筛选目的来说,是性能和资源的最佳权衡。
结论:对于您场景中的“反向判断”,推荐使用 方案B 或 方案C。 方案B实现简单,方案C性能和资源更优。
2. 缓存与数据库数据一致性
“写数据库 + 异步更新/删除缓存” 的策略,保证最终一致性。
异步消费者更新缓存:
一个独立的缓存维护服务消费上述消息,执行以下逻辑:
如果动作是
follow
:对于方案A:执行
SET follow_status:{follower_id}_{followee_id} 1 EX 86400
(设置一个较长的过期时间)。对于方案B:执行
SADD following_set:{follower_id} {followee_id}
。同时,为这个Set设置一个过期时间,或定期刷新。对于方案C:执行
BF.ADD following_bf:{follower_id} {followee_id}
。
如果动作是
unfollow
:对于方案A:执行
DEL follow_status:{follower_id}_{followee_id}
。对于方案B:执行
SREM following_set:{follower_id} {followee_id}
。对于方案C:布隆过滤器不支持删除。这是一个硬伤。解决方案是:
重建一个新的布隆过滤器,同步当前数据库中的最新关注列表,然后替换掉旧的。
或者,接受取消关注后仍有短暂误判,通过设置一个较短的TTL(如几分钟)让缓存自动失效,然后从数据库重建,来达到最终一致。
冷启动:当缓存不存在时(如用户第一次被查询),从数据库
我的关注
表中加载该用户的全部关注列表,并构建上述缓存(Set或布隆过滤器)。兜底策略:如果缓存查询失败(如Redis宕机),则直接降级到数据库查询。虽然慢,但保证了功能的可用性。
前端/客户端 将该用户的最新几条内容预加载下来
预加载核心逻辑:在用户点击“关注”按钮后,前端不仅仅发送一个“关注”的API请求,而是同时(或紧随其后)触发一个获取该用户最新内容的请求。
这两个操作都是后端完成,还是前段并发调用,还是前端串行调用,这就要分析不同选型的利弊了。
方案一:并行请求(最直接、延迟最低)
前端同时并行发起两个异步请求:
- 请求A (关注API):
POST /api/follow?user_id=123
请求B (获取内容API):
GET /api/users/123/posts?limit=3
(获取用户123的最新3条内容)
请求A成功:UI上立即将按钮变为“已关注”,给予用户即时反馈。
请求B成功:前端将获取到的3条内容数据,静默地、动态地插入到当前信息流列表的顶部或一个合适的位置。用户无感知,但滑动时就能立刻看到新内容。
方案二:串行请求(逻辑简单,易于实现)
用户点击“关注”按钮。
前端发起 请求A (关注API):
POST /api/follow?user_id=123
。在 请求A的成功回调 中,立即发起 请求B (获取内容API):
GET /api/users/123/posts?limit=3
。拿到请求B的数据后,再将内容插入到信息流中。
优点: 逻辑清晰,确保关注成功后才去拉取内容。
缺点: 有网络延迟,用户感受到“关注成功”和“看到内容”之间有一个微小的时间差。
方案三:后端聚合(最优雅,对前端最友好)
优点:
极大减少网络延迟:只有一个网络来回。
逻辑内聚:前端无需关心额外的请求逻辑。
更可靠:避免了前端并行或串行请求可能遇到的失败问题。
缺点: 需要后端API进行改造,增加了API的复杂度和响应时间(但增加的耗时是内部服务调用,远小于网络延迟)。
不论哪种方案,需要考虑的细节点:
插入位置:预加载的内容应该插入到信息流的顶部,因为这符合“最新内容”的时间线逻辑。
视觉处理:插入时可以添加一个细微的动画或一个“新内容”的临时标识,让过渡更自然。也可以静默插入,不做任何提示。
内容去重:需要为每条内容设置唯一ID。当后台异步任务将完整的历史内容同步到收件箱后,前端在加载更多信息流时,可能会再次拉取到这些预加载的内容。此时需要根据ID进行去重,避免在信息流中显示重复内容。
失败处理:必须明确,预加载是一个优化手段,而不是核心逻辑。如果获取最新内容的请求失败,绝对不应该影响“关注”操作本身的成功状态。UI上应正常提示“关注成功”,剩余的交由后台异步任务去完成同步。
大V判断规则
1. 基于规则的定时任务(简单可靠)
优点:实现简单,对数据库压力可控(低频批量操作),逻辑清晰。
缺点:有延迟。一个用户粉丝数在上午突破10万,直到第二天凌晨才会被识别为大V,期间对他的Feed流处理仍然是普通用户模式。
2. 基于事件的实时检测(更精准,复杂度高)
触发时机:在 “粉丝数变更” 的地方埋点。这通常发生在“关注”或“取消关注”的异步流水线中,在更新完粉丝数缓存和数据库之后。
在更新粉丝数的服务中,嵌入一个判断逻辑。
如果某个用户的粉丝数跨越了预设的阈值(例如,从99999变成100000,或从100000变成99999),则向消息队列发送一条事件消息。
Topic:
user_relation_change
Message:
{user_id: 123, current_fans_count: 100000, from: 99999}
一个专门的
用户类型维护服务
消费此消息,根据当前粉丝数和规则,决定是否需要更新user_type
字段。优点:近乎实时,用户体验和系统处理逻辑无缝切换。
缺点:
系统复杂性增加:引入了事件驱动机制,需要维护新的服务和Topic。
临界点抖动:一个用户的粉丝数在阈值上下频繁波动时,会导致其
user_type
被频繁更新,进而可能引发Feed流系统的震荡。需要防抖处理(例如,只有当粉丝数稳定超过阈值5分钟后才触发升级)
混合方案(推荐)
基线同步:使用 方案一(定时任务) 作为基线,确保每天所有用户的类型都被校准一次,防止任何事件丢失导致的长久不一致。
实时补偿:使用 方案二(事件检测) 作为补偿,但只处理“从小变大”的单向升级(即普通用户 -> 大V)。
当检测到用户粉丝数首次突破阈值时,实时更新其
user_type
。对于“从大V降级为普通用户”的操作,则交给每天的定时任务来处理。
优点:既保证了核心体验(用户成为大V后能立刻享受正确的服务),又避免了降级时的临界点抖动问题。系统复杂度和稳定性得到了很好的平衡。
总结:“我会采用一种混合策略。首先,一个每日的定时任务作为基准,确保数据的最终正确性。其次,为了更好的用户体验,会引入一个基于事件的实时判断,但只做单向的升级触发(普通->大V),而降级则由每日任务处理,这样可以避免临界点波动。同时,我会将判断规则(如粉丝数阈值)配置化,以便未来业务灵活调整。从长远看,这个分类逻辑本身可以演进为一个独立的、支持策略模式的服务,为整个系统提供统一的用户画像服务。”