Timeline抽象库:统一朋友圈、IM与推送的数据流分发

发布时间:2026/10/27 8:44:36
Timeline抽象库:统一朋友圈、IM与推送的数据流分发

简介:面向社交应用与即时通讯场景的Java抽象库,围绕时间线模式整合朋友圈、微博、消息推送、信息流及即时通讯能力,重点解决数据流在系统内的分发与有序推送问题,适合需要快速搭建社交动态模块或即时通讯基础框架的中高级Java开发者。压缩包内共71个文件,以51个Java源码为主,辅以9个XML配置、3个Markdown文档及少量模板与配置文件,整体大小1.59MB,目录结构清晰,按核心、存储、启动器、示例等模块组织,便于按需抽取。当前已有84人学习下载。该库从时间线核心抽象到基于Redis、内存等存储实现,再到即时通讯与物联网示例工程,提供了从数据建模、推送策略到落地集成的完整参考,开发者可直接复用其分发机制,并可基于业务场景扩展消息过滤、排序与离线处理逻辑,有效降低时间线类功能的自研成本。

1. 先给结论:这类 Timeline 抽象库,到底在抽象什么东西

很多人第一次看到“Timeline 模式朋友圈、微博、消息推送、feed流、im通讯的抽象库”这个描述,会觉得它是个大杂烩——朋友圈、微博、推送、IM 明明是四个不同场景,怎么可能用一个库统一。但做过社交业务的人会立刻意识到:这四个场景背后是同一类数据模型——用户产生内容,内容按时间线聚合,再按关注关系 / 会话关系分发给不同的接收端。朋友圈的时间线是好友维度,微博是关注维度,IM 是会话维度,消息推送是设备维度。它们的数据流形态不一样,但“写入—聚合—分发—读取”这个骨架是完全一致的。

这个库要解决的问题,就是把你从“每个业务各写一套时间线缓存、各维护一套推送队列”的重复劳动里解放出来。它提供一个抽象层:你只需要定义自己的数据模型(比如一条动态、一条消息),声明它属于哪条数据流(朋友圈流、微博流、会话流),然后由这个库负责数据在流之间的搬运、排序、过滤和派发。对于 Java 后端工程师来说,这种库的价值在于:你不用再为每个场景单独造轮子,而且流与流之间的“分发”逻辑,比如你发了一条朋友圈,既要进自己的时间线,又要进所有好友的时间线,还要触发一个推送事件给在线用户,这一连串动作可以被声明式地配置出来。

适合谁?适合正在做社交、社区、即时通讯或消息触达系统的团队。如果你只需要一个简单的发帖列表,用不上它;但如果你面临多条数据流互相联动、消息既要落库又要实时推、不同端看到的时间线顺序还不一样,那这类库就是值得投入的方向。下面我会从它的核心数据模型讲起,再给出一套可落地的 Java 实现思路,最后把最有价值的避坑经验单独拿出来说。

2. Timeline 数据模型的本质:单条流的读写路径,也是多流分发的底座

要理解这个库,先别管它内部用了 Redis 还是 MySQL,先把“时间线”这个抽象概念想清楚。

2.1 一条 Timeline 由什么组成:消息盒子、游标和排序规则

一条 Timeline 可以理解为一个按时间顺序排列的消息盒子。最典型的实现是一个有序集合,key 是 Timeline 的 ID(比如某个用户的 userId),value 是消息 ID,score 是时间戳或者自增序列。读取的时候,从 score 的某个位置开始,取一页。

但这个模型有几个关键参数你必须先定义好。

第一,消息 ID 的生成方式。你不能直接拿数据库自增 ID 当消息 ID 用,因为消息可能从多个数据流汇入,不同源的 ID 会冲突。我一般用“业务源前缀 + 分布式 ID”,比如post:10001、im:20002,这样一条消息在合并排序时才能保证全局唯一。

第二,排序用的 score。标准做法是毫秒级时间戳乘以一个系数,避免同一毫秒内插入多条消息时顺序不稳定。更稳妥的方案是用一个全局自增的序列号,因为时间戳精度不够,而且时钟回拨会引发排序错乱。

第三,拉取游标。客户端每次拉取,需要携带一个游标,表示“我上次看到哪里”。游标通常是消息 ID 或者 score 值。Timeline 库要做的,是把这个游标转成一次精准的区间查询,而不是每次都从头扫。

下面是一个典型的最小读取接口,我习惯这样设计:

public interface Timeline { boolean push(String timelineId, MessageItem item, double score); List<MessageItem> pull(String timelineId, String cursor, int limit); boolean delete(String timelineId, String messageId); }

这段代码看起来简单,但每个方法背后都有代价。push是写操作,可能需要同时写多个 Timeline(后面会讲 fanout);pull是读操作,需要考虑缓存;delete是删除操作,最麻烦,因为你不知道哪条 Timeline 引用了这条消息,尤其是做了 fanout 之后,删除需要反向查找。所以千万别在接口设计阶段忽略这个细节。

2.2 写扩散还是读扩散:这是所有 Timeline 系统的分水岭

设计 Timeline 抽象库时,第一个要决策的不是选什么数据库,而是选写入模型。

读扩散(也叫拉模式)的意思是:每次用户拉取首页动态时,系统先去查它的关注列表,再查这些关注者最近产生的内容,最后在内存里合并排序。这种模型在用户量小的时候很好实现,一亿用户时直接崩——每次请求要打几十次查询,延迟不可控。

写扩散(也叫推模式)的意思是:当用户发一条动态时,系统立刻把这条动态写入所有关注者的时间线里。用户在拉取时,只需要读自己那条 Timeline 就行了,查询是 O(1)。但代价是写放大:一个百万粉丝的博主发一条动态,就要写入百万条记录。而如果你混入 IM 场景,情况更复杂——IM 消息通常是写扩散的,因为要保证双方会话都能实时看到,但群聊的写放大问题会直接打爆存储。

我在实际做方案时,几乎不会纯用某一种模式,而是根据数据流的类型选择混合模式。例如:

  • 朋友圈这种私域关系链,适合写扩散,因为好友数有限,写放大可控;
  • 微博这种公域关注场景,大 V 的粉丝量极大,写扩散性价比低,通常用读扩散 + 热点缓存;
  • IM 单聊,写扩散,因为只有两端;
  • IM 群聊,500 人以内写扩散,5000 人以上选读扩散(拉取群消息时按群维度聚合)。

把这个决策放进抽象库的设计里,核心是要提供一种Timeline 类型配置,让调用方声明某个数据流是“推模式”还是“拉模式”。这个抽象库的分发功能,本质上就是根据配置决定数据写入后往哪里走。

下面模拟一段配置:

TimelineConfig config = TimelineConfig.builder() .id("friendFeed") .writeMode(WriteMode.FANOUT) // 写扩散 .fanoutPolicy(FanoutPolicy.ALL_FOLLOWERS) .scoreSeed(ScoreSeed.SEQUENCE) // 使用全局序列,避免时间戳碰撞 .storage(Storage.REDIS_ZSET) .build();

这里的FanoutPolicy.ALL_FOLLOWERS表示写入后要把消息分发给所有关注者。这是抽象库要解决的核心问题之一:数据流间的分发。除了关注者分发,还有“同步写入另一个流”的分发。比如朋友圈动态除了进好友时间线,还要进“自己的发布历史流”,甚至要触发一条 IM 通知消息——这些都可以抽象为多个 Timeline 之间的链路。

3. 数据流间的分发:把“写一条数据”变成“写一个流程图”

“数据流间的分发功能”是这个标题的点睛之笔。什么叫数据流间分发?举个具体场景:用户 A 发了一条朋友圈,这条数据至少需要被写到三个地方。

  1. 用户 A 自己的“发布记录 Timeline”;
  2. 用户 A 所有好友的“好友动态 Timeline”;
  3. 消息推送服务——给所有在线好友发布一条“你有新动态”的推送事件。

如果不用抽象库,你自己写就会变成:在发帖接口里先插入 MySQL,再写 Redis 的若干 key,再调推送 RPC。代码越来越多,后续新增一个“搜索索引”或“用户聚合流”的时候,你还要回去改发帖接口——这就是典型的耦合。

抽象库的做法是,把“发一条朋友圈”定义为一个数据流图:源节点是post:create,汇入目标是若干个 Timeline 和事件队列。分发器(dispatcher)负责按照图的结构把消息投递到每个目标。

3.1 分发器的核心设计:同步还是异步,决定了系统的成败

分发器要回答的第一个问题是:这条消息的写入要不要同步等待所有目标写成功?

我的经验是:主数据流必须同步,衍生数据流一律异步。“主数据流”指用户打开 App 后最关心能不能立刻看到的数据——比如自己发帖后,自己的发布记录里必须立刻出现;好友的时间线可以异步,因为好友刷到慢几秒没有体验问题。但如果把“自己的发布记录”也做成异步,用户发完刷新半天看不见,就会觉得系统卡了。

所以分发器对外要提供两种 API:dispatchSync和dispatchAsync。库内部维护一个任务队列,异步任务交给线程池或者 MQ 执行。

下面是分发器的一种实现骨架,我通常这样组织:

public class Dispatcher { private final Map<String, Downstream> downstreamRegistry = new ConcurrentHashMap<>(); public void register(String sourceType, Downstream downstream) { downstreamRegistry.put(sourceType, downstream); } public void dispatch(MessageItem item, DispatchContext context) { List<Downstream> downstreams = downstreamRegistry.get(item.getSourceType()); for (Downstream downstream : downstreams) { if (downstream.isAsync()) { taskQueue.submit(() -> downstream.route(item, context)); } else { downstream.route(item, context); // 同步阻塞 } } } }

这个代码的关键在于Downstream接口。它是一个抽象目标,可以是一个 Timeline,也可以是一个推送 RPC,甚至可以是一个 MQ 的 producer。每个Downstream有独立的isAsync()配置。这样就实现了“分发逻辑”和“业务代码”的彻底解耦。

参数说明:sourceType是消息来源类型,比如POST、IM_MSG、SYSTEM_NOTIFY。DispatchContext里携带必要的元数据,比如当前用户 ID、发生时间戳、发起端类型。这里要注意的是:不要让下游直接依赖上游的消息体结构。我们应该让MessageItem持有Map<String, Object> payload,下游需要哪些字段自己取,而不是强类型绑定。否则每加一种业务类型,就要改一次接口。

3.2 分发链路里的数据流转:先路由再过滤,最后逐级写目标

分发器不是简单地把同一个事件广播给所有人。它必须支持路由规则和过滤规则。路由规则决定“这个消息要发给哪些 Timeline”;过滤规则决定“这条消息能不能进入这个 Timeline”。

举个 IM 的例子:用户 A 在群聊里发言,这条消息需要分发到群成员各自的会话 Timeline。但有个点:被禁言的成员、已退群的成员不应该收到。你不能等消息写完了再删除,而是在路由阶段就排除。

我从实际需求里总结了一套配置对象:

public class RouteRule { private String targetTimelineType; // 目标流类型 private String targetIdExtractor; // 从消息里提取目标 Timeline ID,如 "userId" private Predicate<MessageItem> filter; // 过滤条件,返回 false 则不投递 private boolean sync; // 是否同步写 private int priority; // 优先级,影响排序 }

这种配置看起来很像规则引擎,但不需要真的引入规则引擎——用 Java 的Predicate和Function就够。实际项目中,规则可以用 JSON 存到配置中心,动态修改。比如某次活动需要过滤某些关键词,只需改配置,不用发版。

这里要特别提醒:分发体系最怕“回调地狱”式的反向调用。比如分发完一条朋友圈后,你又要触发消息推送,推送服务又要回调查询用户关系。这种回调链如果没有任何收敛,最后会变成蜘蛛网。我惯用的做法是:所有下游都只消费事件对象,不允许下游反向调用原库的查询接口。数据流是单向的,出了问题可以用离线任务对账,但运行时不要形成环。

4. 把朋友圈 / 微博 / IM / 推送四个场景映射到同一套抽象上

这个库的价值体现在它能同时承载四个场景。下面我会分别演示如何把每个业务场景套进这套抽象模型。

4.1 朋友圈:关系链写扩散 + 双向关系校验

朋友圈的最大特点是关系是双向确认的,不存在“单向关注”。这意味着 A 的朋友 Timeline = B 能看到的范围,一定同时满足“A 是 B 的好友”和“B 是 A 的好友”。

以这个抽象库来建模,朋友圈流就是friendFeed:{userId},写扩散时遍历好友列表,把动态写入所有好友的流。有一个坑:好友列表量级典型在几十到几千之间,写扩散完全可行。但删除好友呢?如果 A 删除了 B,B 的 Timeline 里已经写入的 A 的动态必须移除。这个库要提供“删除”的传播能力,也就是当你调用delete(timelineId, messageId)时,还要能反向定位到所有被分发过的 Timeline。常见做法是建一张消息路由表,记录messageId -> [timelineId 列表]。没有这张表,删除只能靠扫描,不可接受。

下面是好友流写入的调用示例:

// 用户A发一条朋友圈 MessageItem post = MessageItem.builder() .messageId("post:abc123") .sourceType("MOMENT") .payload(Map.of("authorId", "U_A", "content", "今天天气不错")) .build(); // 抽象库内部完成:写A自己的流 + 写所有好友的流 + 异步触发推送 timelineRouter.route(post, RouteContext.of("U_A"));

这里的route方法是上进分发器的门面。RouteContext.of("U_A")内部会去加载 A 的好友列表,加载方式是你可以扩展的 SPI。库本身不关心你是从 Redis 还是图数据库拿好友列表,只要你提供一个FriendLoader实现。

4.2 微博/Feed流:读扩散 + 热点缓存兜底

微博这类公域 feed 流,如果也用写扩散,大 V 发一条内容就得给几百万粉丝写时间线,存储和网络的成本都扛不住。所以最常见的是“拉取时聚合”。

把这个抽象库用于微博场景时,你不需要创建大量的followFeed:{userId},而是让每一个用户的首页流逻辑上是一个“聚合查询”。库内部可以把“关注列表”当作一种特殊的数据源,拉取时实时从各被关注者的个人发布流里读取,再在内存里按 score 排序合并。

但纯读扩散在热点场景同样会出问题:如果某大 V 短时间内发了大量内容,拉取时每个人都要去读该大 V 的个人流,热点可能把 Redis 打爆。所以我通常加一层“热点用户墙”:对粉丝数超过阈值(比如 1000 万)的大 V,系统把他最近 100 条消息预合成一个独立缓存流,所有关注他的人,拉取时把这条缓存流当成一个普通上游来并流。这种优化可以放在抽象库的“流提供者”概念里,不用侵入业务代码。

以这个库建模,微博场景如下:

TimelineConfig weiboFeedConfig = TimelineConfig.builder() .id("weiboFollowFeed") .writeMode(WriteMode.READ_ON_DEMAND) // 读扩散 .sourceAggregator(Aggregator.of( up -> up.getFollowingList(up.getUserId(), 100) )) .build();

注意sourceAggregator这个方法:它定义了如何得到一个用户的上游流列表。库在执行拉取时,会调用这个聚合器,拿到用户关注的人的 ID,再去拉取每个人的个人流。

这个抽象方式有一个典型坑:分页游标不能简单复用单流的游标。聚合多流时,游标应该是一个全局时间点,而不是某一个流里的位置。我实现时会把游标编码成一个字符串,包含“截止时间戳 + 已拉取的消息ID集合的哈希”,这样才能避免重复和遗漏。

4.3 IM 通讯:会话流与会话成员流,两种模型的融合

IM 消息流和朋友圈有个本质区别:IM 的消息有明确的“会话属性”。一条会话流(conversationId)应该包含该会话里所有消息,每个成员打开会话时只需要拉取这个会话流,不需要给每个成员写一份相同的消息副本。

但如果你要支持“消息已读回执”“每个会话的未读数”,就仍然需要维护成员维度的状态。我的建议是:消息内容流与会话状态流分开。消息内容流用会话维度,写入一份即可;每个成员维护一个自己的游标,记录“我已读到哪条消息”。这种模型很漂亮地解决了群聊写放大的问题。

// IM 群聊消息 MessageItem imMsg = MessageItem.builder() .messageId("im:group:msg:99") .sourceType("IM_GROUP") .payload(Map.of( "conversationId", "cid:group:101", "senderId", "U_A", "body", "你好,请问今晚几点?" )) .build(); // 写入会话流,不需要逐成员 fanout imConversationStream.push("conv:group:101", imMsg, nowScore);

这里 push 之后,每个群成员在拉取时,会使用自己的游标调用pull("conv:group:101", cursor)。未读数就是 “最近一条消息的 score”与“自己的游标所指向的 score”之间的消息数。

这种模型下,IM 的“分发”不等于“复制消息”,而是“分发游标和通知事件”:消息只存一份,但每个成员收到一个“有新消息”的事件,用于更新未读角标。这个事件本身就可以走抽象库里的另一个数据流——通知流。

这里要重点提醒:IM 消息需要保证顺序一致,尤其在多端同步场景。如果依赖时间戳排序,两个用户发消息时由于客户端时间不同,消息顺序可能错乱。我建议在 IM 流中使用服务端自增序列作为 score,而不是客户端时间。这个库已经支持scoreSeed(ScoreSeed.SEQUENCE),我强烈建议 IM 场景必选它。

4.4 消息推送:把推送目标抽象成一个下游事件流

消息推送(Push Notification)在四个场景里最特殊,因为它的目标是设备或终端,不是 Timeline。但通过抽象库的“分发”概念,你可以把推送处理成一个下游订阅者:当一条数据写入了某条 Timeline,同时触发一个PushEvent写入推送事件流。

这个事件流的消费方可以是极光、APNs、FCM 或自研推送服务。抽象库需要提供的是事件格式的规范,以及保证至少投递一次的机制。

Downstream pushDownstream = new Downstream() { @Override public boolean isAsync() { return true; } @Override public void route(MessageItem item, DispatchContext ctx) { pushService.dispatch( ctx.getUserId(), PushPayload.from(item) ); } }; dispatcher.register("MOMENT", pushDownstream);

这里的pushDownstream被注册为MOMENT消息的下游。当用户发朋友圈时,分发器就会异步触发推送。要注意:推送服务往往有自己的限流策略,比如 Apple Push 每秒限制 1000 条,因此这个下游需要一个信号量或令牌桶来削峰。不要把所有推送压力直接怼到第三方服务,否则会被限流甚至封号。

在这一场景中,如果下游失败了怎么办?库至少要支持“重试队列”的能力:把消息写入一个可恢复的本地表或 MQ,失败后按指数退避重试。这是“数据流分发”最核心的可靠性保证,也是这个库和普通线程池调用最本质的区别。

5. Timeline 抽象库落地避坑:5 个最常见的翻车现场与补救方案

下面这部分是我真正做这类系统时的血泪经验。有些问题你不栽进去很难体会,但提前知道了能省几个通宵。

5.1 坑:用 Redis 的 ZSET 存储所有 Timeline,内存直接爆掉

  • 现象:上线后 Redis 内存涨得特别快,还没到双十一就告警,甚至 OOM。
  • 原因:每个用户的 Timeline 都单独占用一个 ZSET。一亿用户,每个用户即便只存 100 条动态,那也是 100 亿个元素。纯 Redis 内存根本扛不住。
  • 解决:不要把每个 Timeline 都做成一个 Redis key。常见做法是“冷热分离”:只给近 7 天活跃用户建 Redis ZSET;不活跃用户的时间线从 MySQL 或归档存储里查询。或者用 Redis 只存消息 ID 列表,消息正文走其他存储。更重要的一点是,设置 Timeline 的长度上限,例如每个流最多保存 500 条消息,超过部分丢弃或归档。这需要库在push时执行ZREMRANGEBYRANK,相当于加一个“自动裁剪”策略。

5.2 坑:写扩散后更新消息内容,所有副本全是旧数据

  • 现象:用户在朋友圈发了一条动态,后来编辑了内容,但好友刷到的还是旧内容。
  • 原因:写扩散模式下,每条好友 Timeline 里存的是消息 ID,消息正文可能是独立存储。如果编辑时只更新了正文存储,没问题;但如果你在写扩散时把整条消息正文也存进每个 Timeline,那每个副本都需要更新,漏掉一个就脏了一个。
  • 解决:时间线里永远只存消息引用,不存正文快照。拉取时通过消息 ID 批量查询正文缓存。这样动态被编辑时,只需改一份正文源数据。代价是多一次批量查询,但值得。该实现要固化到抽象库的设计约束里:MessageItem进入 Timeline 时只保留 ID 和 score,正文走MessageLoader接口。

5.3 坑:游标设计不正确,拉取时出现重复消息

  • 现象:客户端连续分页拉取时,最后一页重复显示上页最后一条消息,或者遗漏中间消息。
  • 原因:分页游标用“上一页最后一条消息的 score”时,如果多条消息 score 相同,下一页查询会包含重复的边界记录。另一个原因是并发写入时,非原子查询导致拉到不一致快照。
  • 解决:游标不能只用 score,必须加上“偏移量 + 最后一条消息ID”。推荐的游标格式是:score|messageId|偏移。查询时使用ZREVRANGEBYSCORE结合limit,过滤掉游标消息 ID 之前的数据。同时保证同一个用户的多页查询在同一个 Redis 节点上执行,避免分片导致的顺序错乱。

5.4 坑:数据流间分发形成循环,导致消息风暴

  • 现象:A 流写入后触发 B 流写入,B 流的处理逻辑又触发 A 流写入,最终系统整个卡死,消息队列积压。
  • 原因:业务方在写下游route方法时,不小心调用了触发自己这条流的 API。这种问题在分布式中极难排查,因为调用链已经散落在多个服务。
  • 解决:抽象库必须提供分发链路 ID(traceId)和深度限制。每条消息进入分发器时带上唯一的链路 ID,分发器内部维护一个Set<String> seenTraceId,如果一条链路里出现超过 3 次重复流转,直接丢弃并告警。这个保护机制应该内置于库,不能让下游开发者自己控制。

5.5 坑:消息推送失败重试导致发送重复消息,用户骂街

  • 现象:推送下游重试后,用户收到两条一模一样的推送,甚至更多。
  • 原因:下游写库成功,但返回超时,触发重试,结果业务层没有做幂等。
  • 解决:分发器生成的每条事件必须有一个全局握手 ID,下游消费端根据这个 ID 做去重。通常做法是:在推送事件表中建唯一索引(traceId, messageId),重复插入报错后直接忽略。抽象库要保证相同的 traceId 在同一个消息链路中不会变化,否则没法去重。同时对推送下游配置“最多重试 3 次”,超过后转人工或丢弃,尽量不要做无数次的无限重试。

6. 进阶:给这个抽象库加上可观测性和一页纸的验证清单

到了这一层,你已经有了能跑通基础流程的抽象库。但在生产环境里,光能跑通远远不够,你还需要能够回答三个问题:数据丢了没有、延迟多久、哪条链路最脆弱。本节分享我常用的观测设计和一个最简单的压测方法。

6.1 可观测性:为每条数据流打上“水印”

真正的数据流系统,需要一种机制来记录处理进度。我们叫它“水印”:每条 Timeline 在写入数据时会记录一个递增的序号,消费端可以定期检查自己消费到的序号和最新序号之间的差。我建议在抽象库的Timeline接口里增加一个getWatermark(timelineId)方法,返回当前最新落库的序列号。

日常巡检时,运维脚本只需要定时扫描主要流的水印,如果某个流的水印 5 分钟内没变化,说明写入端可能阻塞了。这个方法比看日志更直观。

6.2 加一套快速验证:发一笔模拟数据,验证全链路分发

在你把库落地到业务前,我建议写一个一次性的自检类,模拟一条朋友圈数据的完整分发链路。这个测试不需要连通真实推送服务,只需要验证三个点:消息不丢、不重、顺序基本一致。

@Test void verifyDistributionFlow() { // 准备两个测试好友 String friendId1 = "testFriend1"; String friendId2 = "testFriend2"; // 发布朋友圈 MessageItem msg = MessageItem.builder() .messageId("post:test001") .sourceType("MOMENT") .payload(Map.of("authorId", "testAuthor")) .build(); // 分发器按配置把消息写到所有好友流 timelineRouter.route(msg, RouteContext.of("testAuthor")); // 验证好友1的Timeline里出现了这条消息 List<MessageItem> list1 = timelineRouter.pull( "friendFeed:" + friendId1, null, 10 ); Assertions.assertTrue( list1.stream().anyMatch(m -> m.messageId().equals("post:test001")) ); // 验证作者自己的发布流里也有 List<MessageItem> ownList = timelineRouter.pull( "selfFeed:testAuthor", null, 10 ); Assertions.assertTrue( ownList.stream().anyMatch(m -> m.messageId().equals("post:test001")) ); }

这个测试的价值在于:它一次性覆盖了“写作者流”和“写好友流”两个关键路径。如果之后有人改了路由配置导致好友流漏写,这个测试会立刻失败。我建议把它放到 CI 里,每次代码变更都跑一次。

6.3 参数与习惯:我把哪些配置项默认调成这样

做了几个项目后,我把一些关键的默认参数定下来了,如果你的业务没有特殊理由,可以照抄:

  • 单个 Timeline 默认最大长度 500 条,超过 500 就用 LREM 裁剪旧消息。对于大多数信息流产品,用户翻到 500 条之前的概率极低。
  • 写扩散的 fanout 批量数每批不要超过 500 个目标,否则会导致写超时。如果好友数很大,拆成多个批量异步执行。
  • 读扩散聚合时,每次最多聚合 30 个上游流。超过 30 个,先合并其中活跃度最高的 20 个,剩下的流按优先级取时间戳最近的消息合入。
  • 异步分发线程池的拒绝策略用CallerRunsPolicy,不要用DiscardPolicy——丢弃消息是绝对不可接受的。

最后说个习惯:我在接入这种抽象库时,总是要求团队成员把“消息不丢”作为第一原则,其次才考虑“不重”和“顺序”。很多业务可以接受偶发重复推送,但无法接受用户做了一次操作却任何终端都没反应。所以我在系统里为所有分布式场景设计了唯一的traceId,并且要求任何下游重试都必须携带这个 ID。这套做法不是纯理论,而是在多个生产项目里被验证过。希望我的这套经验能帮你少走一段冤枉路。

本文还有配套的精品资源,点击获取