好友系统设计
概述
好友是社交类游戏留存的核心:好友一起开黑、互赠体力、比拼排行。好友系统的难点在请求流程的状态管理(申请/同意/拒绝/拉黑)与大规模关系下的查询性能。本文设计完整的好友系统:好友请求/添加/删除/拉黑、好友列表在线状态、好友上限管理、推荐好友算法。
一、好友关系模型
1.1 关系状态
| 状态 | 含义 | 说明 |
|---|---|---|
| NONE | 无关系 | 默认 |
| APPLYING | 已申请未同意 | 单向申请记录 |
| FRIEND | 好友 | 双向确认 |
| BLACK | 拉黑 | 单向屏蔽 |
设计要点:
好友 = 双向关系(双方确认)
申请 = 单向待处理记录
拉黑 = 单向(被拉黑方不知情,按业务定)1.2 存储模型
好友关系表(推荐一行存一对):
player_friend:
player_id BIGINT
friend_id BIGINT
relation TINYINT # 1 好友 2 拉黑
group_name VARCHAR
create_time BIGINT
PRIMARY KEY(player_id, friend_id)
好友请求表:
player_friend_request:
id BIGINT
from_id BIGINT
to_id BIGINT
status TINYINT # 0 待处理 1 同意 2 拒绝
create_time BIGINT
KEY idx_to(to_id, status)分片考虑:
好友按 player_id 分片(Uid Hash)
查询"我的好友" = 单分片查询
避免全表扫二、好友请求流程
2.1 申请与处理
流程:
1. A 申请加 B(校验:非自己、非已有好友、B 未拉黑 A)
2. 生成申请记录(待处理)
3. 通知 B(在线推送 / 离线邮件)
4. B 处理:
同意 → 双向写 FRIEND 关系 + 通知 A
拒绝 → 更新申请状态
忽略 → 保留待处理(可过期)
异常分支:
重复申请 → 提示"已申请,等待对方处理"
对方上限 → 拒绝并提示
申请过期(如 7 天)→ 自动失效java
public class FriendService {
public void apply(long fromId, long toId) {
if (fromId == toId) {
return;
}
if (isFriend(fromId, toId)) {
return; // 已是好友
}
if (isBlack(toId, fromId)) {
return; // 对方拉黑了我
}
if (requestDao.existsPending(fromId, toId)) {
return; // 已有待处理申请
}
requestDao.insert(fromId, toId);
notify(toId, "好友申请", fromId);
}
public void accept(long toId, long fromId) {
// 校验申请存在
if (requestDao.updateStatus(fromId, toId, STATUS_ACCEPT) > 0) {
friendDao.insert(fromId, toId, REL_FRIEND);
friendDao.insert(toId, fromId, REL_FRIEND);
notify(fromId, "好友申请已同意", toId);
}
}
}双向写入保证一致性:
双方各存一行 FRIEND
查"我的好友"只需查自己那一行
删除/拉黑操作单行即可(或双向处理)2.2 删除与拉黑
删除好友:
双方关系行删除(或单方删除 + 对方列表同步)
广播"关系变化"(客户端刷新列表)
被删方可再申请
拉黑:
写入 BLACK 关系(黑名单)
拉黑后:
对方申请 → 直接拒绝
对方消息 → 屏蔽(按业务)
解除拉黑 → 恢复 NONE拉黑实现细节:
黑名单即 relation=BLACK 的记录
校验"对方是否拉黑我" → 查对方行
聊天屏蔽在聊天服务层二次过滤三、在线状态
3.1 状态来源
在线状态 = 玩家当前连接状态
数据源:SessionManager(在线集合)/ Redis
好友列表展示需要"好友是否在线"
性能问题:
好友列表查每人状态 → 批量查询
Redis 批量 MGET 在线标记
或服务器内存缓存在线集合3.2 状态推送
状态变化通知:
上线 → 通知好友"xxx 上线了"
下线 → 通知好友"xxx 下线了"
实现:
上下线事件 → 广播给自己的好友列表
好友多 → 批量推送(粉丝多时量大)
优化:好友在线才推(不在线不用通知)
状态展示:
客户端拉取好友列表时带状态
订阅式更新(在线期间状态变化推送)四、好友上限管理
4.1 上限设计
上限规则(按付费/等级可扩):
免费玩家:50 好友
VIP 玩家:100-200 好友
申请上限:待处理申请 20 条
实现:
好友数 = 关系表 count
申请时校验好友数 < 上限
上限变化(付费)→ 超限部分保留不删4.2 上限校验位置
校验时机:
添加好友时校验
接受申请时校验(对方可能满)
批量导入(活动)→ 统一校验
错误处理:
超出上限 → 拒绝 + 提示"好友已满"
被拒申请方 → 提示"对方好友已满"五、推荐好友算法
5.1 推荐维度
| 维度 | 说明 | 数据源 |
|---|---|---|
| 共同好友 | 好友的好友 | 好友关系 |
| 同公会 | 公会成员 | 公会表 |
| 同局对战 | 最近对战对手 | 对战记录 |
| 活跃度 | 在线/近期活跃 | 登录日志 |
| 兴趣标签 | 玩法偏好 | 行为数据 |
5.2 推荐算法
简化推荐(休闲游戏):
共同好友数排序(强社交信号)
同公会/同局补位
过滤:已是好友、已拉黑、自己
批量推荐:
每日/每周生成推荐候选池
列表接口从池中取(离线计算,避免实时图计算)
进阶(社交图谱):
好友的拓扑传播(一度/二度)
图数据库(Neo4j)/ 离线图计算
休闲游戏用"共同好友 + 活跃"足够java
public class FriendRecommendService {
public List<Long> recommend(long playerId, int limit) {
// 1. 候选:共同好友 + 同公会 + 同局
Map<Long, Integer> candidates = new HashMap<>();
for (long friend : friendDao.friendIds(playerId)) {
for (long fof : friendDao.friendIds(friend)) {
if (fof != playerId && !isFriend(playerId, fof)) {
candidates.merge(fof, 1, Integer::sum); // 共同好友数
}
}
}
// 2. 按共同好友数排序取 TopN
return candidates.entrySet().stream()
.sorted(Map.Entry.<Long, Integer>comparingByValue().reversed())
.limit(limit)
.map(Map.Entry::getKey)
.toList();
}
}推荐合规:
不推荐已拉黑用户
未成年人社交保护(按合规配置)
推荐结果可反馈(不感兴趣 → 过滤)六、好友列表接口
接口设计:
getFriendList(playerId) → 好友 + 分组 + 在线状态
getRequests(playerId) → 待处理申请
applyFriend(toId)
acceptRequest(fromId) / rejectRequest(fromId)
deleteFriend(friendId)
blacklist(playerId) / unblacklist(playerId)
recommendFriends()
性能:
好友列表分页加载(大列表)
在线状态批量查(Redis MGET)
列表变更推送(客户端缓存更新)七、常见问题
| 问题 | 处理 |
|---|---|
| 关系不一致(单方好友) | 双向写入 + 对账 |
| 申请堆积 | 上限 + 过期清理 |
| 在线状态不准 | 心跳 + 状态推送 |
| 好友上限绕过 | 服务端校验 |
| 推荐重复 | 过滤已处理 + 去重 |
八、小结
好友系统的核心是关系状态机 + 双向一致性:好友关系用"双方各存一行"保证查询简单与一致,请求流程以状态记录驱动(申请/同意/拒绝/过期);在线状态来自会话层,批量查询与订阅推送结合;上限管理在添加与接受两个入口校验;推荐算法以"共同好友 + 同公会 + 同局"为信号,离线生成候选池。社交关系是长期资产,数据一致性、状态实时性与性能三者都要兼顾。