为什么会写这篇文章?
近期正在完成零工系统中职位推荐的功能,为了能让用户能够有个性化推荐、实时更新、提高用户参与度等友好的体验,最终以feed流的方式呈现给用户。
(零工系统中,通过用户收藏的职位、浏览的职位、发布的简历、投递的职位、历史完成的订单等多维度进行不同权重加权并计算,结合TimeLine(职位发布的时间倒序,即取最新的职位)设计智能推荐功能。游客模式,则通过点击次数最多的职位、收藏次数最多的职位、完工订单最多的职位进行加权并结合TimeLine进行推荐。
智能推荐+TimeLine结合的方式,实现起来比较简单,同时又能在一定程度上避免"信息茧房"问题。("信息茧房":只关注自己感兴趣的信息,从而被限制到这一小范围之中))
个性化推荐:
Feed 流能够根据用户的行为数据(如浏览历史、搜索记录、简历信息等)进行个性化的职位推荐。例如,如果用户之前浏览过软件开发相关的职位,系统可以在 Feed 流中推送更多同领域的职位,像前端开发、后端开发、软件测试等不同细分方向的岗位。
实时更新展示:
职位信息在招聘市场中是动态变化的,新的职位不断涌现,旧的职位可能很快就招满。Feed 流可以实时推送最新的职位信息,让求职者第一时间了解到市场上的职位动态。
提高用户参与度:
其类似于社交媒体的呈现方式,能够吸引用户的注意力。职位以卡片或者信息流的形式展示,用户可以方便地浏览、点赞、评论或者分享职位信息。
1. 什么是Feed流?
Feed流(信息流)是一种常见的内容展示方式,用户可以在一个连续的列表中浏览由系统推送的内容。这些内容可以是新闻、动态、图片、视频等。典型的应用场景包括社交媒体平台(如微博、微信朋友圈)、新闻客户端(如今日头条)、短视频平台(如抖音)等。
简单来说,其实就是能够实时/智能推送信息的数据流。
Feed流本质上来说是一个数据流,是将"N个发布者的信息单元"通过"关注关系"传递给"M个接收者"。
2. Feed流的核心特点
- 个性化推荐:根据用户的兴趣、行为和社交关系,推送符合用户偏好的内容。
- 实时性:内容更新频繁,用户可以随时获取最新的信息。
- 无限滚动:用户可以通过上下滑动不断加载更多内容,体验流畅。
3. Feed流的几种常见形式
3.1 时间线(Timeline)
时间线是最简单的 Feed 流形式,按照时间顺序展示内容。例如,微博的时间线会按照发布时间先后顺序展示用户的动态。
伪代码:
java
// 时间线排序示例
public class TimelineFeed {
private List<Post> posts = new ArrayList<>();
public void addPost(Post post) {
posts.add(post);
Collections.sort(posts, Comparator.comparing(Post::getCreatedAt).reversed());
}
public List<Post> getPosts() {
return posts;
}
}
class Post {
private String content;
private LocalDateTime createdAt;
// 构造函数、getter 和 setter 省略
}
3.2 关注流(Follow Stream)
关注流只展示用户关注的对象发布的内容。例如,微博的"关注"页面只会显示用户关注的博主发布的动态。
伪代码:
java
// 关注流示例
public class FollowStream {
private Map<User, Set<User>> followRelations = new HashMap<>();
private Map<User, List<Post>> userPosts = new HashMap<>();
public void follow(User follower, User followed) {
followRelations.computeIfAbsent(follower, k -> new HashSet<>()).add(followed);
}
public List<Post> getFollowStream(User user) {
Set<User> followedUsers = followRelations.getOrDefault(user, Collections.emptySet());
List<Post> stream = new ArrayList<>();
for (User followed : followedUsers) {
stream.addAll(userPosts.getOrDefault(followed, Collections.emptyList()));
}
Collections.sort(stream, Comparator.comparing(Post::getCreatedAt).reversed());
return stream;
}
}
class User {
private String username;
// 构造函数、getter 和 setter 省略
}
3.3 推荐流(Recommendation Stream)
推荐流基于算法推荐内容,通常结合用户的兴趣、历史行为和社交关系进行个性化推荐。例如,抖音的"推荐"页面会根据用户的观看历史推荐相似的视频。
其中,需要注意的是,推荐流需要依赖推荐系统,推荐质量的好坏与推荐算法有直接关系。
推荐系统的相关文献把它们分为三类:
- 协同过滤系统(仅使用用户与商品的交互信息生成推荐)
- 基于内容系统(利用用户偏好/或商品偏好)
- 混合推荐模型系统(使用交互信息、用户和商品的元数据)
零工推荐使用的是混合推荐模型系统,通过用户对职位的投递与收藏对其进行深度分析,从而进行推荐。
伪代码:
java
// 推荐流示例
public class RecommendationStream {
private Map<User, List<Post>> recommendedPosts = new HashMap<>();
public void recommendPosts(User user, List<Post> posts) {
recommendedPosts.put(user, posts);
}
public List<Post> getRecommendedPosts(User user) {
return recommendedPosts.getOrDefault(user, Collections.emptyList());
}
}
4. 设计Feed流系统的注意事项
4.1 数据量与性能
信息流拉取性能直接应用用户的使用体验。随着用户数量和内容数量的增加,Feed 流的数据量会迅速增长。聚合这么多的数据就需要查询多次缓存、数据库、计数器,而在每秒几十万次的请求下,如何保证在100ms之内完成这些查询操作,在技术上是比较大的挑战。因此,必须考虑如何高效地存储和查询数据,避免性能瓶颈。常用的优化手段包括分库分表、缓存、索引等。
4.2 实时性与延迟
Feed 流要求内容能够快速推送给用户,尤其是在热点事件或突发新闻时。为了保证实时性,可以采用消息队列、异步处理等方式来加速数据流转。
4.3 高并发
以微博为例,信息流是微博的主体模块,是用户进入到微博之后最先看到的模块,因此它的并发请求量是最高的,可以达到每秒几十万次请求。
4.4 用户隐私与安全
在设计 Feed 流时,必须确保用户数据的安全性和隐私保护。敏感信息应加密存储,访问权限严格控制,防止数据泄露。
4.5 可扩展性
随着业务的发展,Feed 流系统需要具备良好的可扩展性,支持水平扩展和垂直扩展。微服务架构、分布式数据库等技术可以帮助实现这一目标。
5. Feed流架构设计
5.1 拉取模式(Pull Model)
拉取模式是指客户端主动向服务器请求最新内容。这种方式简单易实现,但存在频繁请求导致的带宽浪费和服务器负载过高的问题。
拉取模式存储成本虽然比较低,但是查询和聚合这两个操作的成本会比较高。尤其是对于单个用户关注了很多人的情况来说,需要定时获取他关注的所有人的动态然后再做聚合,成本很高。
适用于产品前期,数据量比较小的情况。另外,拉取模式下的数据流的实时性要比推模式差。
伪代码:
java
// 拉取模式示例
public class PullFeedService {
private List<Post> posts = new ArrayList<>();
public List<Post> fetchNewPosts(User user) {
// 模拟从数据库中获取新内容
return posts.stream()
.filter(post -> post.getCreatedAt().isAfter(user.getLastFetchTime()))
.collect(Collectors.toList());
}
}
5.2 推送模式(Push Model)
推送模式是指服务器主动将新内容推送给客户端。这种方式可以减少客户端的频繁请求,降低带宽消耗,提高用户体验。常用的技术包括 WebSocket、长轮询、Server-Sent Events (SSE) 等。
推模式中对同步库的要求只有一个:写入能力强。
但是推送模式下,存储成本是比较高的。比如一个微博大V每发一条动态,就需要将动态插入到每个粉丝对应的feed表中,存储成本可想而知。
伪代码:
java
// 推送模式示例(使用WebSocket)
@WebSocketEndpoint("/feed")
public class PushFeedService {
@OnOpen
public void onOpen(Session session) {
// 注册新的连接
System.out.println("New client connected");
}
@OnMessage
public void onMessage(String message, Session session) {
// 处理客户端发送的消息
System.out.println("Received message: " + message);
}
@OnClose
public void onClose(Session session) {
// 移除断开的连接
System.out.println("Client disconnected");
}
public void pushNewPost(Post post) {
// 将新内容推送给所有连接的客户端
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(post.toJson());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
5.3 混合模式(Hybrid Model)
混合模式结合了拉取和推送的优点,既保证了实时性,又减少了不必要的请求。例如,客户端可以定期拉取一次内容,同时通过 WebSocket 接收实时推送。
以微博为例,混合模式的核心其实是针对微博大V和不活跃用户的特殊处理。首先,我们需要判断出哪些用户属于该大V的活跃用户与不活跃用户;针对活跃用于采用推送模式,对于不活跃用户采用自己拉取的模式。
大部分用户的消息都是写扩散,只有大V是读扩散,这样既控制了资源浪费,又减少了系统设计复杂度。但是整体设计复杂度还是要比推模式复杂。
伪代码:
java
// 混合模式示例
public class HybridFeedService {
private final PullFeedService pullService = new PullFeedService();
private final PushFeedService pushService = new PushFeedService();
public void initialize(User user) {
// 初始化时拉取一次内容
List<Post> initialPosts = pullService.fetchNewPosts(user);
// 同时启动推送服务
pushService.onOpen(user.getSession());
}
public void handleNewPost(Post post) {
// 新内容到达时,先推送给客户端
pushService.pushNewPost(post);
// 定期拉取以确保完整性
pullService.fetchNewPosts(user);
}
}
6. Feed流的存储
为了构建一个高效、可扩展且可靠的 Feed 流系统,选择合适的存储方案至关重要。不同的数据类型和访问模式需要不同的存储技术来优化性能和成本。通过查阅资料总结出以下多种存储需求的可行方案:
6.1 用户与元数据存储(关系型数据库)
选择:MySQL 或 PostgreSQL
用户信息、帖子元数据等结构化数据最适合存储在关系型数据库中。这些数据库提供了强大的事务支持和复杂的查询能力,确保数据的一致性和完整性。
示例:用户和帖子表设计
sql
-- 创建用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建帖子表
CREATE TABLE posts (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 创建关注关系表
CREATE TABLE follows (
follower_id BIGINT NOT NULL,
followed_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (follower_id, followed_id),
FOREIGN KEY (follower_id) REFERENCES users(id),
FOREIGN KEY (followed_id) REFERENCES users(id)
);
6.2 动态内容存储(NoSQL 数据库)
选择:MongoDB
动态内容如评论、点赞、分享等半结构化数据适合存储在 NoSQL 数据库中。MongoDB 支持灵活的文档模型,能够高效处理高并发写入,并且易于水平扩展。
示例:帖子评论存储
javascript
// 使用MongoDB存储帖子评论
const mongoose = require('mongoose');
const CommentSchema = new mongoose.Schema({
postId: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
content: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
const Comment = mongoose.model('Comment', CommentSchema);
6.3 大文件存储(分布式文件系统)
选择:HDFS 或 Ceph
对于大文件(如图片、视频),应使用分布式文件系统进行存储。这些系统提供了高可用性和容错能力,确保数据的安全性和可靠性。同时,它们可以轻松应对大规模数据存储的需求。
示例:使用 HDFS 存储图片
python
# 使用HDFS存储图片
from hdfs import InsecureClient
client = InsecureClient('http://namenode:50070', user='hadoop')
with open('local_image.jpg', 'rb') as f:
client.upload('/user/hadoop/remote_image.jpg', f)
6.4 热点数据缓存(Redis)
选择:Redis
为了提高频繁访问数据的查询效率,可以使用 Redis 进行缓存。Redis 是一个高性能的内存数据库,支持多种数据结构(如字符串、哈希、列表等),并且具有持久化功能,确保数据不会因服务器重启而丢失。
示例:缓存热门帖子
java
// 使用Redis缓存热门帖子
public class RedisCacheService {
private Jedis jedis = new Jedis("localhost");
public void cachePopularPosts(List<Post> posts) {
for (Post post : posts) {
jedis.set("post:" + post.getId(), post.toJson());
}
}
public Post getCachedPost(long postId) {
String json = jedis.get("post:" + postId);
if (json != null) {
return Post.fromJson(json);
}
return null;
}
}
6.5 实时数据处理与分析(消息队列 + 流处理框架)
选择:Kafka + Flink
为了实现高效的实时数据处理和分析,可以结合使用消息队列(如 Kafka)和流处理框架(如 Apache Flink)。Kafka 负责数据的可靠传输,Flink 则用于实时处理和分析流数据,生成个性化推荐结果或监控系统状态。
示例:使用 Kafka 和 Flink 处理实时数据
java
// 使用Kafka生产者发送新帖子事件
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
producer.send(new ProducerRecord<>("new_posts", "post_id", "post_content"));
producer.close();
// 使用Flink消费Kafka中的数据并进行实时处理
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStream<String> stream = env.addSource(new FlinkKafkaConsumer<>("new_posts", new SimpleStringSchema(), props));
stream.map(post -> {
// 处理逻辑
return post;
}).print();
env.execute("Real-time Post Processing");
7. 总结
Feed 流系统是现代互联网应用中不可或缺的一部分,它为用户提供个性化的信息展示和实时的内容更新。本文详细介绍了 Feed 流的基础概念、常见形式以及设计架构中的关键点。通过合理的推送模式选择和高效的存储方案,可以构建出高性能、可扩展的 Feed 流系统,满足不同应用场景的需求。