跳到主要内容 C++微服务 UserServer 设计与实现 | 极客日志
C++
C++微服务 UserServer 设计与实现 本文记录了 IM 项目中 C++ 用户服务(UserServer)的设计与实现过程。核心职责包括用户认证、信息管理及其他服务的基础支撑。架构上采用依赖注入模式,通过构造函数注入短信、数据库、缓存等外部依赖,便于替换模拟服务及单元测试。功能实现涵盖注册校验、手机号验证码(Mock 服务)、登录会话管理(Redis)及用户信息修改(MySQL+ES+ 文件服务)。实战中解决了 ODB 代码生成遗漏、Redis 初始化顺序及依赖库链接不全等问题。设计强调无状态、依赖注入优先及分层清晰,确保可扩展性与可测试性。
开源信徒 发布于 2026/3/28 更新于 2026/4/14 3 浏览
实战 C++ 微服务:IM 项目用户服务(UserServer)设计与落地全记录
做 IM 项目时,用户服务(UserServer)是整个系统的基石 —— 所有业务(好友、消息、朋友圈)都依赖用户认证和基础信息。这篇文章就从实战角度,聊聊我是怎么设计、实现 UserServer 的,包括核心功能落地、依赖替换(比如用模拟短信服务替代真实平台)、以及那些踩过的坑,希望能给做 C++ 后端的朋友一些参考。
一、先搞懂:UserServer 在 IM 系统里的角色 在之前的 IM 微服务架构里,UserServer 承担 3 个核心职责:
用户认证 :注册(用户名 / 手机号)、登录(用户名密码 / 手机验证码)、会话管理;
用户信息管理 :头像、昵称、签名、手机号的修改与查询;
基础支撑 :给其他服务提供用户信息(比如好友服务查好友资料、消息服务查发送者信息)。
所以设计时,必须考虑可扩展性 (比如后续加第三方登录)、可测试性 (比如不用真实短信也能测手机号登录)、性能 (登录会话用 Redis 缓存,避免查库)。
二、核心设计:从依赖到架构,拒绝'硬编码'
1. 依赖注入:让服务更灵活(踩过坑才懂的重要性) 最开始写 UserServiceImpl 的时候,我直接在类里 new 了 DMSClient(真实短信服务),后来发现个人开发者没法申请企业短信资质,想换成模拟服务时,改了大半天代码。后来重构时,把所有外部依赖都通过构造函数注入,这才清爽了。
UserServiceImpl (const MockSmsClient::ptr& mock_sms_client,
const std::shared_ptr<elasticlient::Client>& es_client,
const std::shared_ptr<odb::core::database>& mysql_client,
const std::shared_ptr<sw::redis::Redis>& redis_client,
const ServiceManager::ptr& channel_manager,
const std::string& file_service_name
) : _es_user(std::make_shared <ESUser>(es_client)),
_mysql_user(std::make_shared <UserTable>(mysql_client)),
_redis_session(std::make_shared <Session>(redis_client)),
_redis_status(std::make_shared <Status>(redis_client)),
_redis_codes(std::make_shared <Codes>(redis_client)),
_file_service_name(file_service_name),
_mm_channels(channel_manager),
_dms_client(mock_sms_client)
{
_es_user->createIndex ();
}
替换依赖不碰业务代码 :把 MockSmsClient 换成真实 DMSClient,只需要改构建器(UserServerBuilder)的初始化逻辑,UserServiceImpl 里的 GetPhoneVerifyCode 完全不用动;
测试方便 :写单元测试时,能注入'假的 Redis 客户端''假的 ES 客户端',不用依赖真实中间件。
2. 核心依赖拆解:每个组件各司其职 依赖组件 作用 实战细节 MockSmsClient 模拟短信发送 内部不调用外部平台,只打印日志 + 存 Redis 验证码 ODB(MySQL ORM) 用户数据 CRUD 需用 odb 工具生成 ORM 代码,避免手写 SQL Redis(sw::redis++) 会话存储、验证码、登录状态 会话过期设 2 小时,验证码 5 分钟过期 Elasticlient 用户搜索(比如好友搜索) 初始化时创建 user 索引,支持昵称 / 手机号模糊查 ServiceManager 调用其他微服务(如文件服务上传头像) 基于 Etcd 发现服务节点,RR 轮询负载均衡
三、核心功能落地:从代码到业务,讲透细节
1. 用户注册:不只是存数据,还要做校验 注册逻辑看起来简单,但细节容易出问题,比如密码强度、昵称重复。看关键代码:
bool password_check (const std::string &password) {
if (password.size () < 6 || password.size () > 15 ) {
LOG_ERROR ("密码长度不合法:{}-{}" , password, password.size ());
return false ;
}
for (int i = 0 ; i < password.size (); i++) {
if (!((password[i] > 'a' && password[i] < 'z' ) ||
(password[i] > 'A' && password[i] < 'Z' ) ||
(password[i] > '0' && password[i] < '9' ) ||
password[i] == '_' || password[i] == '-' )) {
LOG_ERROR ("密码字符不合法:{}" , password);
return false ;
}
}
return true ;
}
void UserRegister (...) {
std::string nickname = request->nickname ();
std::string password = request->password ();
if (!nickname_check (nickname)) {
return err_response ("用户名长度不合法!" );
}
if (!password_check (password)) {
return err_response ("密码格式不合法!" );
}
auto user = _mysql_user->select_by_nickname (nickname);
if (user) {
return err_response ("用户名被占用!" );
}
std::string uid = uuid ();
user = std::make_shared <User>(uid, nickname, password);
if (!_mysql_user->insert (user)) {
return err_response ("Mysql 数据库新增数据失败!" );
}
if (!_es_user->appendData (uid, "" , nickname, "" , "" )) {
return err_response ("ES 搜索引擎新增数据失败!" );
}
response->set_success (true );
}
实战踩坑 :最开始没做密码校验,测试时输入特殊字符导致数据库存储异常,后来加了严格的字符校验,还在日志里打印不合法的密码,方便排查问题。
2. 手机号验证码:用模拟服务突破平台限制 真实短信服务(如阿里云 DMS)需要企业资质,个人开发没法用,所以做了 MockSmsClient 来模拟。核心逻辑是:生成验证码→存 Redis→返回验证码 ID,校验时从 Redis 查。
第一步:设计 MockSmsClient
class MockSmsClient {
public :
using ptr = std::shared_ptr<MockSmsClient>;
MockSmsClient (const Codes::ptr& codes_client) : _codes_client(codes_client) {}
bool send (const std::string& phone, const std::string& code) {
LOG_INFO ("【模拟短信】向{}发送验证码:{}" , phone, code);
return true ;
}
private :
Codes::ptr _codes_client;
};
第二步:集成到 GetPhoneVerifyCode void GetPhoneVerifyCode (...) {
std::string phone = request->phone_number ();
if (!phone_check (phone)) {
return err_response ("手机号码格式错误!" );
}
std::string code_id = uuid ();
std::string code = vcode ();
if (!_dms_client->send (phone, code)) {
return err_response ("短信验证码发送失败!" );
}
_redis_codes->append (code_id, code, std::chrono::minutes (5 ));
response->set_verify_code_id (code_id);
response->set_success (true );
}
关键优势 :后来要对接真实短信服务时,只需要实现一个 RealSmsClient,保持 send 接口一致,在 UserServerBuilder 里换个注入对象就行,业务代码一行不用改。
3. 登录会话管理:Redis 防多端登录 登录成功后,要生成会话 ID(ssid),存 Redis,还要标记用户登录状态,防止同一账号多端登录:
void UserLogin (...) {
auto user = _mysql_user->select_by_nickname (nickname);
if (!user || password != user->password ()) {
return err_response ("用户名或密码错误!" );
}
if (_redis_status->exists (user->user_id ())) {
return err_response ("用户已在其他地方登录!" );
}
std::string ssid = uuid ();
_redis_session->append (ssid, user->user_id (), std::chrono::hours (2 ));
_redis_status->append (user->user_id (), std::chrono::hours (2 ));
response->set_login_session_id (ssid);
response->set_success (true );
}
细节 :_redis_session 和 _redis_status 是封装的 Redis 操作类,内部调用 sw::redis::Redis::set 并设置过期时间,避免手动写 Redis 命令,减少出错概率。
4. 用户信息修改:联动多存储(MySQL+ES + 文件服务) 以'设置头像'为例,需要上传头像到文件服务→更新 MySQL 的 avatar_id→同步 ES 信息:
void SetUserAvatar (...) {
std::string uid = request->user_id ();
std::string avatar_data = request->avatar ();
auto user = _mysql_user->select_by_id (uid);
if (!user) {
return err_response ("未找到用户信息!" );
}
auto channel = _mm_channels->choose (_file_service_name);
FileService_Stub stub (channel.get()) ;
PutSingleFileReq file_req;
PutSingleFileRsp file_rsp;
file_req.mutable_file_data ()->set_file_content (avatar_data);
stub.PutSingleFile (&cntl, &file_req, &file_rsp, nullptr );
if (cntl.Failed () || !file_rsp.success ()) {
return err_response ("文件子服务调用失败!" );
}
std::string avatar_id = file_rsp.file_info ().file_id ();
user->avatar_id (avatar_id);
if (!_mysql_user->update (user)) {
return err_response ("更新数据库用户头像 ID 失败!" );
}
if (!_es_user->appendData (user->user_id (), user->phone (), user->nickname (), user->description (), avatar_id)) {
return err_response ("更新搜索引擎用户头像 ID 失败!" );
}
response->set_success (true );
}
经验 :文件服务调用可能失败,后来加了重试机制(失败后重试 2 次),还在日志里打印文件服务的地址和错误信息,方便定位是网络问题还是服务本身的问题。
四、常见问题与解决方案
1. ODB 代码生成遗漏导致链接错误 最开始用 ODB 的 ORM,只写了 user.hxx,没生成 ORM 实现代码,编译时出现一堆 undefined reference to odb::access::object_traits_impl<zrt::User> 错误。
安装 ODB 工具:sudo apt install odb;
生成 ORM 代码:odb -d mysql --std c++11 user.hxx -o source/;
CMake 里添加生成的 user-odb.cxx 到源文件列表:
add_executable(user_server source/user_server.cc source/user-odb.cxx # 必须加,否则链接不到 ORM 实现 )
2. Redis 客户端初始化顺序错误 最开始在 make_redis_object 里创建了 Codes 实例,但没赋值给 _codes_client,导致 make_mock_sms_object 时 _codes_client 是空的,运行崩溃。
解决方法 :调整初始化顺序,确保 Redis 客户端先初始化,再创建 Codes 和 MockSmsClient:
void make_redis_object (...) {
_redis_client = RedisClientFactory::create (...);
_codes_client = std::make_shared <Codes>(_redis_client);
}
void make_mock_sms_object () {
_mock_sms_client = std::make_shared <MockSmsClient>(_codes_client);
}
3. 依赖库链接不全导致未定义错误 编译时出现 undefined reference to sw::redis::Redis::set,是因为 CMake 没链接 swredis++ 和 hiredis 库。
# 查找依赖库
find_package(swredis++ REQUIRED)
find_package(hiredis REQUIRED)
# 链接到目标
target_link_libraries(user_server PRIVATE sw::redis++::swredis++ hiredis::hiredis odb::mysql # ODB MySQL 库 elasticlient::elasticlient # ES 客户端库 brpc # RPC 库 pthread # 线程库 )
五、总结:UserServer 设计的 3 个核心要点
无状态设计 :用户服务不存本地数据(会话、状态都放 Redis),方便横向扩展,加节点就能扛更高并发;
依赖注入优先 :所有外部依赖(短信、数据库、缓存)都通过构造函数注入,方便替换和测试,比如用模拟短信突破平台限制;
分层清晰 :业务逻辑(注册登录)、数据访问(ODB/Redis/ES)、服务调用(ServiceManager)分层,修改某一层不影响其他层。
做用户服务时,最容易忽略的是'可测试性'和'容错性'—— 比如一开始没做模拟短信,导致没法本地测试手机号登录;没加重试机制,文件服务偶尔超时就失败。这些问题都是实战中踩出来的,比单纯的代码实现更有价值。
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online