一、服务定位与技术栈
在即时通讯(IM)系统中,好友管理子服务是连接'用户社交关系'与'聊天会话'的核心枢纽。它既要处理好友申请与关系维护,也要管理单聊/群聊会话的创建与成员维护。本文基于实际项目代码(C++/brpc/Protobuf/ODB),从接口设计、数据模型、核心逻辑、高可用部署四个维度,完整拆解好友管理子服务的实现细节。
一、服务定位与技术栈 在即时通讯(IM)系统中,**好友管理子服务**是连接'用户社交关系'与'聊天会话'的核心枢纽。它既要处理好友申请与关系维护,也要管理单聊/群聊会话的创建与成员维护。基于实际项目代码(C++/brpc/Protobuf/ODB),从接口设计、数据模型、核心逻辑、高可用部署四个维度,完整拆解好友管理子服务的实现细节。 核心业务范围 **好友关系**:申请、同意/拒绝、删除、…

在即时通讯(IM)系统中,好友管理子服务是连接'用户社交关系'与'聊天会话'的核心枢纽。它既要处理好友申请与关系维护,也要管理单聊/群聊会话的创建与成员维护。本文基于实际项目代码(C++/brpc/Protobuf/ODB),从接口设计、数据模型、核心逻辑、高可用部署四个维度,完整拆解好友管理子服务的实现细节。
| 技术组件 | 作用说明 | 项目中的封装/使用场景 |
|---|---|---|
| brpc | 高性能 RPC 框架 | 搭建 RPC 服务器,提供 Protobuf 接口 |
| Protobuf | 数据序列化/接口定义 | 定义服务接口(如 FriendAdd/GetChatSessionList) |
| ODB | MySQL ORM 框架 | 映射数据库表到 C++ 对象(如 RelationTable) |
| MySQL | 关系型数据库 | 存储好友关系、会话信息、申请事件 |
| Elasticsearch | 全文搜索引擎 | 实现用户模糊搜索,过滤已加好友和当前用户 |
| Etcd | 服务注册与发现 | 注册自身服务,发现用户服务、消息服务地址 |
| gflags | 命令行参数解析 | 配置服务端口、数据库地址、日志等级等 |
| spdlog | 日志框架 | 输出调试/错误日志,支持文件/控制台双输出 |
微服务的核心是接口化协作,好友管理子服务的对外交互全靠 Protobuf 定义的接口。以下结合项目代码,拆解核心接口的设计逻辑。
好友服务共提供 10 个核心接口,覆盖'好友关系''会话管理''搜索'三大场景,接口设计遵循单一职责原则:
| 接口名称 | 业务场景 | 核心作用 |
|---|---|---|
GetFriendList | 查看好友列表 | 返回当前用户的所有好友信息(昵称、头像等) |
FriendAdd | 发送好友申请 | 校验关系后创建申请事件,返回事件 ID |
FriendAddProcess | 处理好友申请 | 同意则创建好友关系+单聊会话,拒绝则删除事件 |
FriendRemove | 删除好友 | 移除好友关系+单聊会话+会话成员 |
FriendSearch | 搜索用户 | ES 模糊搜索,过滤已加好友和当前用户 |
GetPendingFriendEventList | 查看待处理申请 | 返回当前用户收到的所有未处理好友申请 |
GetChatSessionList | 查看聊天会话列表 | 返回单聊/群聊会话,包含最新消息 |
ChatSessionCreate | 创建群聊会话 | 生成群聊 ID,添加会话信息和成员 |
GetChatSessionMember | 查看群成员 | 返回指定群聊的所有成员信息 |
FriendAdd:发送好友申请message FriendAddReq {
string request_id = 1; // 链路追踪ID(分布式调用唯一标识)
string user_id = 2; // 申请人ID(当前登录用户)
string respondent_id = 3; // 被申请人ID(目标用户)
}
message FriendAddRsp {
string request_id = 1; // 对应请求ID
bool success = 2; // 申请是否成功
string errmsg = 3; // 错误信息(失败时填充)
string notify_event_id = 4; // 申请事件ID(用于后续处理)
}
业务逻辑:
用户 A 申请加 B 为好友时,网关会先鉴权并填充 user_id(A 的 ID),服务端需校验:
relation 表);friend_apply 表);notify_event_id(UUID),存入 friend_apply 表。FriendAddProcess:处理好友申请message FriendAddProcessReq {
string request_id = 1; // 链路追踪ID
string user_id = 2; // 被申请人ID(当前登录用户)
string apply_user_id = 3; // 申请人ID(A的ID)
bool agree = 4; // 是否同意(true=同意,false=拒绝)
string notify_event_id = 5; // 申请事件ID(对应FriendAdd返回的ID)
}
message FriendAddProcessRsp {
string request_id = 1; // 对应请求ID
bool success = 2; // 处理是否成功
string errmsg = 3; // 错误信息
string new_session_id = 4; // 同意时生成的单聊会话ID(网关推送用)
}
业务逻辑: 用户 B 处理 A 的申请时,服务端需:
notify_event_id 对应的申请是否存在;relation 表插两条记录)、单聊会话(chat_session 表)、会话成员(chat_session_member 表插 A 和 B 的记录)。好友服务的核心数据存储在 MySQL 中,通过 ODB(Object-Relational Mapping) 框架将 C++ 对象与数据库表关联,避免直接编写 SQL 语句,提升代码可维护性。
| 数据库表名 | ODB 映射类 | 核心字段(C++/MySQL) | 业务作用 |
|---|---|---|---|
relation(好友关系表) | RelationTable | _user_id(varchar64)/_peer_id(varchar64) | 存储双向好友关系(A→B 和 B→A) |
friend_apply(申请事件表) | FriendApplyTable | _event_id(varchar64)/_user_id/_peer_id | 存储好友申请的生命周期(待处理/已处理) |
chat_session(会话表) | ChatSessionTable | _chat_session_id(varchar64)/_session_type(tinyint) | 存储单聊/群聊会话元信息(类型、名称) |
chat_session_member(会话成员表) | ChatSessionMemberTable | _chat_session_id/_user_id | 存储'会话-用户'关联(群聊成员、单聊双方) |
// mysql_relation.hpp(ODB 映射类)
#pragma once
#include <odb/core.hxx>
#include <string>
namespace zrt {
// 好友关系实体类(对应 relation 表)
#pragma db object table("relation")
class Relation {
public:
Relation() {}
Relation(const std::string& user_id, const std::string& peer_id)
: _user_id(user_id), _peer_id(peer_id) {}
std::string user_id() const { return _user_id; }
void user_id(const std::string& v) { _user_id = v; }
std::string peer_id() const { return _peer_id; }
void peer_id(const std::string& v) { _peer_id = v; }
private:
friend class odb::access;
#pragma db id auto // 自增主键
unsigned long _id;
#pragma db type("varchar(64)") index // 加索引,加速查询
std::string _user_id; // 主动方用户ID
#pragma db type("varchar(64)")
std::string _peer_id; // 被动方用户ID
};
// ODB 表操作类(封装 CRUD)
class RelationTable {
public:
using ptr = std::shared_ptr<RelationTable>;
RelationTable(const std::shared_ptr<odb::core::database>& db) : _db(db) {}
// 插入双向好友关系(A→B 和 B→A)
bool insert(const std::string& user_id, const std::string& peer_id) {
try {
odb::transaction t(_db->begin());
_db->persist(Relation(user_id, peer_id));
_db->persist(Relation(peer_id, user_id));
t.commit();
return true;
} catch (const odb::exception& e) {
LOG_ERROR("Insert relation failed: {}", e.what());
return false;
}
}
// 检查好友关系是否存在
bool exists(const std::string& user_id, const std::string& peer_id) {
try {
odb::transaction t(_db->begin());
auto count = _db->query_value<Relation>(
odb::query<Relation>::_user_id == user_id &&
odb::query<Relation>::_peer_id == peer_id
).count();
t.commit();
return count > 0;
} catch (const odb::exception& e) {
LOG_ERROR("Check relation exists failed: {}", e.what());
return false;
}
}
// 其他方法:删除关系、查询用户的所有好友ID(省略)
private:
std::shared_ptr<odb::core::database> _db;
};
} // namespace zrt
关键设计:
A→B 和 B→A),确保双方查询好友列表时都能找到对方;transaction 确保'插入两条记录'要么全成功,要么全失败,避免数据不一致;_user_id 加索引,查询'用户的所有好友'时性能显著提升。好友服务的核心逻辑封装在 FriendServiceImpl 类中,通过'接口实现+私有方法'的方式实现业务解耦与代码复用。
FriendServiceImpl 的构造函数通过依赖注入(DI)传入所有外部依赖,避免硬编码,便于测试与扩展:
FriendServiceImpl(
const std::shared_ptr<elasticlient::Client>& es_client, // ES 客户端
const std::shared_ptr<odb::core::database>& mysql_client, // MySQL 客户端
const ServiceManager::ptr& channel_manager, // RPC 信道管理器
const std::string& user_service_name, // 用户服务名称
const std::string& message_service_name // 消息服务名称
) : _es_user(std::make_shared<ESUser>(es_client)),
_mysql_apply(std::make_shared<FriendApplyTable>(mysql_client)),
_mysql_chat_session(std::make_shared<ChatSessionTable>(mysql_client)),
_mysql_chat_session_member(std::make_shared<ChatSessionMemberTable>(mysql_client)),
_mysql_relation(std::make_shared<RelationTable>(mysql_client)),
_user_service_name(user_service_name),
_message_service_name(message_service_name),
_mm_channels(channel_manager) {}
解耦设计:
RelationTable/FriendApplyTable 封装 MySQL 操作,业务逻辑不直接依赖 ODB;ServiceManager 管理其他服务的 RPC 信道,避免直接硬编码服务地址(服务地址变化时,只需更新 Etcd,无需改代码)。FriendAddProcess 涉及'申请事件删除、好友关系创建、会话创建'三个关键步骤,需确保数据一致性:
void FriendServiceImpl::FriendAddProcess(
::google::protobuf::RpcController* controller,
const ::zrt::FriendAddProcessReq* request,
::zrt::FriendAddProcessRsp* response,
::google::protobuf::Closure* done) {
brpc::ClosureGuard rpc_guard(done); // 自动释放 Closure
// 1. 定义错误回调
auto err_response = [this, response](const std::string& rid, const std::string& errmsg) {
response->set_request_id(rid);
response->set_success(false);
response->set_errmsg(errmsg);
};
// 2. 提取请求关键参数
std::string rid = request->request_id();
std::string eid = request->notify_event_id();
std::string uid = request->user_id(); // 被申请人(B)
std::string pid = request->apply_user_id(); // 申请人(A)
bool agree = request->agree();
// 3. 校验申请事件是否存在
if (!_mysql_apply->exists(pid, uid)) {
LOG_ERROR("{}-未找到{}-{}的好友申请事件", rid, pid, uid);
return err_response(rid, "申请事件不存在");
}
// 4. 删除申请事件(无论同意/拒绝,事件都需清理)
if (!_mysql_apply->remove(pid, uid)) {
LOG_ERROR("{}-删除申请事件{}-{}失败", rid, pid, uid);
return err_response(rid, "处理申请失败");
}
// 5. 同意:创建好友关系、单聊会话、会话成员
std::string session_id;
if (agree) {
if (!_mysql_relation->insert(uid, pid)) {
LOG_ERROR("{}-创建好友关系{}-{}失败", rid, uid, pid);
return err_response(rid, "添加好友失败");
}
session_id = uuid(); // 生成全局唯一会话ID
ChatSession session(session_id, "", ChatSessionType::SINGLE);
if (!_mysql_chat_session->insert(session)) {
LOG_ERROR("{}-创建单聊会话{}失败", rid, session_id);
return err_response(rid, "创建会话失败");
}
std::vector<ChatSessionMember> members = {
ChatSessionMember(session_id, uid),
ChatSessionMember(session_id, pid)
};
if (!_mysql_chat_session_member->append(members)) {
LOG_ERROR("{}-添加会话成员{}-{}失败", rid, uid, pid);
return err_response(rid, "添加会话成员失败");
}
}
// 6. 组织响应
response->set_request_id(rid);
response->set_success(true);
response->set_new_session_id(session_id);
LOG_INFO("{}-处理好友申请成功:{}→{},agree={}", rid, pid, uid, agree);
}
关键逻辑亮点:
err_response 回调函数,确保所有错误场景的响应格式一致;好友服务本身不存储用户信息和消息数据,需通过 RPC 调用其他服务获取。
bool FriendServiceImpl::GetUserInfo(
const std::string& rid,
const std::unordered_set<std::string>& uid_list,
std::unordered_map<std::string, UserInfo>& user_list) {
auto channel = _mm_channels->choose(_user_service_name);
if (!channel) {
LOG_ERROR("{}-获取用户服务信道失败", rid);
return false;
}
GetMultiUserInfoReq req;
GetMultiUserInfoRsp rsp;
req.set_request_id(rid);
for (const auto& uid : uid_list) {
req.add_users_id(uid); // 批量添加用户ID
}
brpc::Controller cntl;
zrt::UserService_Stub stub(channel.get());
stub.GetMultiUserInfo(&cntl, &req, &rsp, nullptr);
if (cntl.Failed() || !rsp.success()) {
LOG_ERROR("{}-批量获取用户信息失败", rid);
return false;
}
for (const auto& item : rsp.users_info()) {
user_list.emplace(item.first, item.second);
}
return true;
}
优化点:批量调用减少网络开销;ServiceManager 自动管理用户服务地址(从 Etcd 发现),服务扩缩容时无需重启。
bool FriendServiceImpl::GetRecentMsg(
const std::string& rid,
const std::string& session_id,
MessageInfo& msg) {
auto channel = _mm_channels->choose(_message_service_name);
if (!channel) {
LOG_ERROR("{}-获取消息服务信道失败", rid);
return false;
}
GetRecentMsgReq req;
GetRecentMsgRsp rsp;
req.set_request_id(rid);
req.set_chat_session_id(session_id);
req.set_msg_count(1);
brpc::Controller cntl;
zrt::MsgStorageService_Stub stub(channel.get());
stub.GetRecentMsg(&cntl, &req, &rsp, nullptr);
if (cntl.Failed() || !rsp.success()) {
LOG_WARN("{}-获取会话{}最新消息失败", rid, session_id);
return false;
}
if (rsp.msg_list_size() > 0) {
msg.CopyFrom(rsp.msg_list(0));
return true;
}
return false;
}
业务价值:查询会话列表时显示'每条会话的最新消息',符合微服务'数据私有'原则。
好友服务通过 FriendServerBuilder 模式封装初始化流程,支持'配置解析→依赖构建→服务启动'的一站式部署。
class FriendServerBuilder {
public:
void make_es_object(const std::vector<std::string>& host_list) {
_es_client = ESClientFactory::create(host_list);
}
void make_mysql_object(const std::string& user, const std::string& pswd,
const std::string& host, const std::string& db,
const std::string& cset, int port, int conn_pool_count) {
_mysql_client = ODBFactory::create(user, pswd, host, db, cset, port, conn_pool_count);
}
void make_discovery_object(const std::string& reg_host, const std::string& base_service,
const std::string& user_service_name, const std::string& message_service_name) {
_mm_channels = std::make_shared<ServiceManager>();
_mm_channels->declared(user_service_name);
_mm_channels->declared(message_service_name);
auto on_service_online = std::bind(&ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);
auto on_service_offline = std::bind(&ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);
_service_discoverer = std::make_shared<Discovery>(reg_host, base_service, on_service_online, on_service_offline);
}
void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads) {
_rpc_server = std::make_shared<brpc::Server>();
FriendServiceImpl* service = new FriendServiceImpl(_es_client, _mysql_client, _mm_channels, _user_service_name, _message_service_name);
if (_rpc_server->AddService(service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE) != 0) {
LOG_ERROR("添加好友服务到 RPC 服务器失败");
abort();
}
brpc::ServerOptions options;
options.idle_timeout_sec = timeout;
options.num_threads = num_threads;
if (_rpc_server->Start(port, &options) != 0) {
LOG_ERROR("RPC 服务器启动失败(端口:{})", port);
abort();
}
}
void make_registry_object(const std::string& reg_host, const std::string& service_name, const std::string& access_host) {
_registry_client = std::make_shared<Registry>(reg_host);
_registry_client->registry(service_name, access_host);
}
FriendServer::ptr build() {
if (!_es_client || !_mysql_client || !_rpc_server) {
LOG_ERROR("服务依赖未初始化完成");
abort();
}
return std::make_shared<FriendServer>(_service_discoverer, _registry_client, _es_client, _mysql_client, _rpc_server);
}
private:
Registry::ptr _registry_client;
std::shared_ptr<elasticlient::Client> _es_client;
std::shared_ptr<odb::core::database> _mysql_client;
ServiceManager::ptr _mm_channels;
Discovery::ptr _service_discoverer;
std::shared_ptr<brpc::Server> _rpc_server;
};
int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true);
zrt::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
zrt::FriendServerBuilder fsb;
fsb.make_es_object({FLAGS_es_host});
fsb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host,
FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);
fsb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_user_service, FLAGS_message_service);
fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
fsb.make_registry_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
auto server = fsb.build();
server->start();
return 0;
}
部署关键参数:
FLAGS_mysql_pool_count:MySQL 连接池大小(建议设为 CPU 核心数的 2~4 倍);FLAGS_rpc_threads:brpc IO 线程数(通常设为 CPU 核心数);FLAGS_access_host:服务对外访问地址(需与网关在同一网络)。ServiceManager 管理 RPC 信道,服务地址变化只需更新 Etcd;数据层通过 ODB 封装,更换数据库不影响业务逻辑。relation 表中添加 remark 字段;blacklist 表,支持屏蔽非好友消息;chat_session_member 表中添加 role 字段;user_id 对 MySQL 表进行分片,避免单表数据量过大。好友管理子服务作为 IM 系统的核心组件,通过'Proto 接口定义规范通信、ODB 映射隔离数据层、ServiceManager 解耦服务协作、Builder 模式简化部署',实现了一个高可用、可扩展的微服务。其设计思路不仅适用于 IM 系统,也可复用在社交、电商等需要'关系管理'的业务场景中。该服务在满足业务需求的同时,通过解耦与高可用设计确保了系统的稳定性与可维护性,为后续扩容与迭代奠定了坚实基础。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online