社交系统设计
2026/1/15大约 3 分钟
社交系统设计
场景一:关注关系
需求:实现用户关注/取关、粉丝列表、关注列表、共同关注。
数据结构
-- 关系表
CREATE TABLE user_follow (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, -- 关注者
follow_id BIGINT NOT NULL, -- 被关注者
created_at DATETIME,
UNIQUE KEY uk_user_follow (user_id, follow_id),
KEY idx_follow_id (follow_id)
);Redis 存储
// 关注列表:ZSet(按时间排序)
// following:{userId} -> {followId: timestamp}
// 粉丝列表:ZSet
// followers:{userId} -> {followerId: timestamp}
// 关注
public void follow(Long userId, Long followId) {
long now = System.currentTimeMillis();
// 添加到关注列表
redisTemplate.opsForZSet().add("following:" + userId, followId, now);
// 添加到对方粉丝列表
redisTemplate.opsForZSet().add("followers:" + followId, userId, now);
// 异步写数据库
mqTemplate.send("user-follow", new FollowMessage(userId, followId));
}
// 取关
public void unfollow(Long userId, Long followId) {
redisTemplate.opsForZSet().remove("following:" + userId, followId);
redisTemplate.opsForZSet().remove("followers:" + followId, userId);
}
// 共同关注
public Set<Long> commonFollowing(Long userId1, Long userId2) {
return redisTemplate.opsForZSet().intersect(
"following:" + userId1,
"following:" + userId2
);
}
// 是否关注
public boolean isFollowing(Long userId, Long followId) {
return redisTemplate.opsForZSet().score("following:" + userId, followId) != null;
}场景二:Feed 流
需求:用户看到关注的人发布的动态,按时间倒序。
推模式(写扩散)
// 发布动态时,推送到所有粉丝的收件箱
public void publish(Long userId, Post post) {
// 保存动态
postMapper.insert(post);
// 获取粉丝列表
Set<Long> followers = redisTemplate.opsForZSet().range("followers:" + userId, 0, -1);
// 推送到粉丝收件箱
for (Long followerId : followers) {
redisTemplate.opsForZSet().add("inbox:" + followerId, post.getId(), post.getCreateTime());
}
}
// 获取 Feed
public List<Post> getFeed(Long userId, int page, int size) {
Set<Long> postIds = redisTemplate.opsForZSet().reverseRange(
"inbox:" + userId,
(page - 1) * size,
page * size - 1
);
return postMapper.selectByIds(postIds);
}优点:读取快
缺点:大V发布时写入量大
拉模式(读扩散)
// 发布动态
public void publish(Long userId, Post post) {
postMapper.insert(post);
// 只存到自己的发件箱
redisTemplate.opsForZSet().add("outbox:" + userId, post.getId(), post.getCreateTime());
}
// 获取 Feed(聚合关注的人的动态)
public List<Post> getFeed(Long userId, int page, int size) {
Set<Long> followings = redisTemplate.opsForZSet().range("following:" + userId, 0, -1);
// 聚合多个发件箱
List<String> keys = followings.stream()
.map(id -> "outbox:" + id)
.collect(Collectors.toList());
// 使用 ZUNIONSTORE 或应用层聚合
// ...
}优点:写入简单
缺点:读取时聚合慢
推拉结合
// 大V(粉丝>10万)用拉模式
// 普通用户用推模式
public void publish(Long userId, Post post) {
postMapper.insert(post);
Long followerCount = redisTemplate.opsForZSet().size("followers:" + userId);
if (followerCount > 100000) {
// 大V:只存发件箱
redisTemplate.opsForZSet().add("outbox:" + userId, post.getId(), post.getCreateTime());
} else {
// 普通用户:推送到粉丝收件箱
pushToFollowers(userId, post);
}
}场景三:消息系统
需求:实现私信、群聊、消息已读未读。
私信
// 消息存储
public void sendMessage(Long fromId, Long toId, String content) {
Message msg = new Message(fromId, toId, content);
messageMapper.insert(msg);
// 会话列表更新
String conversationId = getConversationId(fromId, toId);
redisTemplate.opsForZSet().add("conversations:" + fromId, conversationId, System.currentTimeMillis());
redisTemplate.opsForZSet().add("conversations:" + toId, conversationId, System.currentTimeMillis());
// 未读数+1
redisTemplate.opsForHash().increment("unread:" + toId, conversationId, 1);
// WebSocket 推送
webSocketService.push(toId, msg);
}
// 获取会话列表
public List<Conversation> getConversations(Long userId) {
Set<String> conversationIds = redisTemplate.opsForZSet()
.reverseRange("conversations:" + userId, 0, 19);
// 查询最后一条消息、未读数等
}
// 标记已读
public void markRead(Long userId, String conversationId) {
redisTemplate.opsForHash().delete("unread:" + userId, conversationId);
}群聊
// 群消息:写扩散到每个成员
public void sendGroupMessage(Long groupId, Long fromId, String content) {
GroupMessage msg = new GroupMessage(groupId, fromId, content);
groupMessageMapper.insert(msg);
// 获取群成员
Set<Long> members = redisTemplate.opsForSet().members("group:members:" + groupId);
for (Long memberId : members) {
if (!memberId.equals(fromId)) {
// 未读数+1
redisTemplate.opsForHash().increment("group:unread:" + memberId, groupId.toString(), 1);
// WebSocket 推送
webSocketService.push(memberId, msg);
}
}
}场景四:点赞系统
// 点赞
public void like(Long userId, Long postId) {
String key = "likes:" + postId;
// 添加到点赞集合
redisTemplate.opsForSet().add(key, userId);
// 点赞数+1
redisTemplate.opsForHash().increment("post:stats", postId + ":likes", 1);
// 异步写数据库
mqTemplate.send("post-like", new LikeMessage(userId, postId));
}
// 取消点赞
public void unlike(Long userId, Long postId) {
redisTemplate.opsForSet().remove("likes:" + postId, userId);
redisTemplate.opsForHash().increment("post:stats", postId + ":likes", -1);
}
// 是否点赞
public boolean isLiked(Long userId, Long postId) {
return redisTemplate.opsForSet().isMember("likes:" + postId, userId);
}
// 点赞数
public Long getLikeCount(Long postId) {
return (Long) redisTemplate.opsForHash().get("post:stats", postId + ":likes");
}