C++微服務UserServer設計與實現(xiàn)方法詳解
前言
做 IM 項目時,用戶服務(UserServer)是整個系統(tǒng)的基石 —— 所有業(yè)務(好友、消息、朋友圈)都依賴用戶認證和基礎信息。這篇文章就從實戰(zhàn)角度,聊聊我是怎么設計、實現(xiàn) UserServer 的,包括核心功能落地、依賴替換(比如用模擬短信服務替代真實平臺)、以及那些踩過的坑,希望能給做 C++ 后端的朋友一些參考。
一、先搞懂:UserServer 在 IM 系統(tǒng)里的角色
在之前的 IM 微服務架構里,UserServer 承擔 3 個核心職責:
用戶認證:注冊(用戶名 / 手機號)、登錄(用戶名密碼 / 手機驗證碼)、會話管理;
用戶信息管理:頭像、昵稱、簽名、手機號的修改與查詢;
基礎支撐:給其他服務提供用戶信息(比如好友服務查好友資料、消息服務查發(fā)送者信息)。
所以設計時,必須考慮可擴展性(比如后續(xù)加第三方登錄)、可測試性(比如不用真實短信也能測手機號登錄)、性能(登錄會話用 Redis 緩存,避免查庫)。
二、核心設計:從依賴到架構,拒絕 “硬編碼”
1. 依賴注入:讓服務更靈活(踩過坑才懂的重要性)
最開始寫 UserServiceImpl 的時候,我直接在類里 new 了 DMSClient(真實短信服務),后來發(fā)現(xiàn)個人開發(fā)者沒法申請企業(yè)短信資質,想換成模擬服務時,改了大半天代碼。后來重構時,把所有外部依賴都通過構造函數(shù)注入,這才清爽了。
看核心構造函數(shù):
UserServiceImpl(
const MockSmsClient::ptr& mock_sms_client, // 短信服務(真實/模擬可替換)
const std::shared_ptr<elasticlient::Client>& es_client, // ES(用戶搜索用)
const std::shared_ptr<odb::core::database>& mysql_client, // MySQL(用戶數(shù)據(jù)存儲)
const std::shared_ptr<sw::redis::Redis>& redis_client, // Redis(會話/驗證碼)
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) // 注入短信服務,而非內部new
{
_es_user->createIndex(); // 初始化ES用戶索引
}
這樣設計的好處:
替換依賴不碰業(yè)務代碼:把
MockSmsClient換成真實DMSClient,只需要改構建器(UserServerBuilder)的初始化邏輯,UserServiceImpl里的GetPhoneVerifyCode完全不用動;測試方便:寫單元測試時,能注入 “假的 Redis 客戶端”“假的 ES 客戶端”,不用依賴真實中間件。
2. 核心依賴拆解:每個組件各司其職
| 依賴組件 | 作用 | 實戰(zhàn)細節(jié) |
|---|---|---|
| MockSmsClient | 模擬短信發(fā)送 | 內部不調用外部平臺,只打印日志 + 存 Redis 驗證碼 |
| ODB(MySQL ORM) | 用戶數(shù)據(jù) CRUD | 需用odb工具生成 ORM 代碼,避免手寫 SQL |
| Redis(sw::redis++) | 會話存儲、驗證碼、登錄狀態(tài) | 會話過期設 2 小時,驗證碼 5 分鐘過期 |
| Elasticlient | 用戶搜索(比如好友搜索) | 初始化時創(chuàng)建user索引,支持昵稱 / 手機號模糊查 |
| ServiceManager | 調用其他微服務(如文件服務上傳頭像) | 基于 Etcd 發(fā)現(xiàn)服務節(jié)點,RR 輪詢負載均衡 |
三、核心功能落地:從代碼到業(yè)務,講透細節(jié)
1. 用戶注冊:不只是存數(shù)據(jù),還要做校驗
注冊邏輯看起來簡單,但細節(jié)容易出問題,比如密碼強度、昵稱重復??搓P鍵代碼:
bool password_check(const std::string &password) {
// 密碼規(guī)則:6-15位,只含字母、數(shù)字、_、-
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(...) {
// 1. 取請求參數(shù)
std::string nickname = request->nickname();
std::string password = request->password();
// 2. 校驗昵稱、密碼
if (!nickname_check(nickname)) {
return err_response("用戶名長度不合法!");
}
if (!password_check(password)) {
return err_response("密碼格式不合法!");
}
// 3. 查昵稱是否已存在(ODB ORM調用)
auto user = _mysql_user->select_by_nickname(nickname);
if (user) {
return err_response("用戶名被占用!");
}
// 4. 生成用戶ID,存MySQL+ES
std::string uid = uuid(); // 自定義工具函數(shù),生成唯一ID
user = std::make_shared<User>(uid, nickname, password);
if (!_mysql_user->insert(user)) {
return err_response("Mysql數(shù)據(jù)庫新增數(shù)據(jù)失敗!");
}
if (!_es_user->appendData(uid, "", nickname, "", "")) {
return err_response("ES搜索引擎新增數(shù)據(jù)失??!");
}
// 5. 返回成功
response->set_success(true);
}
實戰(zhàn)踩坑:最開始沒做密碼校驗,測試時輸入特殊字符導致數(shù)據(jù)庫存儲異常,后來加了嚴格的字符校驗,還在日志里打印不合法的密碼,方便排查問題。
2. 手機號驗證碼:用模擬服務突破平臺限制
真實短信服務(如阿里云 DMS)需要企業(yè)資質,個人開發(fā)沒法用,所以做了MockSmsClient來模擬。核心邏輯是:生成驗證碼→存 Redis→返回驗證碼 ID,校驗時從 Redis 查。
第一步:設計 MockSmsClient
// mock_sms.hpp
class MockSmsClient {
public:
using ptr = std::shared_ptr<MockSmsClient>;
MockSmsClient(const Codes::ptr& codes_client) : _codes_client(codes_client) {}
// 與真實DMSClient接口完全一致,方便替換
bool send(const std::string& phone, const std::string& code) {
// 不調用外部平臺,只打印日志(測試時能直接看到驗證碼)
LOG_INFO("【模擬短信】向{}發(fā)送驗證碼:{}", phone, code);
return true;
}
private:
Codes::ptr _codes_client; // 用于后續(xù)擴展,比如存驗證碼
};
第二步:集成到 GetPhoneVerifyCode
void GetPhoneVerifyCode(...) {
// 1. 校驗手機號格式(11位,以1開頭,第二位3-9)
std::string phone = request->phone_number();
if (!phone_check(phone)) {
return err_response("手機號碼格式錯誤!");
}
// 2. 生成4位驗證碼(自定義工具函數(shù)vcode())
std::string code_id = uuid();
std::string code = vcode(); // 返回如"1234"
// 3. 調用模擬短信服務(實際只打日志)
if (!_dms_client->send(phone, code)) {
return err_response("短信驗證碼發(fā)送失敗!");
}
// 4. 存Redis(5分鐘過期)
_redis_codes->append(code_id, code, std::chrono::minutes(5));
// 5. 返回驗證碼ID
response->set_verify_code_id(code_id);
response->set_success(true);
}
關鍵優(yōu)勢:后來要對接真實短信服務時,只需要實現(xiàn)一個RealSmsClient,保持send接口一致,在UserServerBuilder里換個注入對象就行,業(yè)務代碼一行不用改。
3. 登錄會話管理:Redis 防多端登錄
登錄成功后,要生成會話 ID(ssid),存 Redis,還要標記用戶登錄狀態(tài),防止同一賬號多端登錄:
void UserLogin(...) {
// 1. 校驗用戶名密碼
auto user = _mysql_user->select_by_nickname(nickname);
if (!user || password != user->password()) {
return err_response("用戶名或密碼錯誤!");
}
// 2. 查是否已登錄(Redis查登錄狀態(tài))
if (_redis_status->exists(user->user_id())) {
return err_response("用戶已在其他地方登錄!");
}
// 3. 生成ssid,存Redis(2小時過期)
std::string ssid = uuid();
_redis_session->append(ssid, user->user_id(), std::chrono::hours(2));
// 4. 標記登錄狀態(tài)(2小時過期,與會話同步)
_redis_status->append(user->user_id(), std::chrono::hours(2));
// 5. 返回ssid
response->set_login_session_id(ssid);
response->set_success(true);
}
細節(jié):_redis_session和_redis_status是封裝的 Redis 操作類,內部調用sw::redis::Redis::set并設置過期時間,避免手動寫 Redis 命令,減少出錯概率。
4. 用戶信息修改:聯(lián)動多存儲(MySQL+ES + 文件服務)
以 “設置頭像” 為例,需要上傳頭像到文件服務→更新 MySQL 的 avatar_id→同步 ES 信息:
void SetUserAvatar(...) {
// 1. 取用戶ID和頭像數(shù)據(jù)
std::string uid = request->user_id();
std::string avatar_data = request->avatar();
// 2. 查用戶是否存在
auto user = _mysql_user->select_by_id(uid);
if (!user) {
return err_response("未找到用戶信息!");
}
// 3. 調用文件服務上傳頭像(通過ServiceManager找文件服務節(jié)點)
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("文件子服務調用失敗!");
}
// 4. 更新MySQL的avatar_id
std::string avatar_id = file_rsp.file_info().file_id();
user->avatar_id(avatar_id);
if (!_mysql_user->update(user)) {
return err_response("更新數(shù)據(jù)庫用戶頭像ID失敗!");
}
// 5. 同步ES信息
if (!_es_user->appendData(user->user_id(), user->phone(),
user->nickname(), user->description(), avatar_id)) {
return err_response("更新搜索引擎用戶頭像ID失敗!");
}
response->set_success(true);
}
經(jīng)驗:文件服務調用可能失敗,后來加了重試機制(失敗后重試 2 次),還在日志里打印文件服務的地址和錯誤信息,方便定位是網(wǎng)絡問題還是服務本身的問題。
四、實戰(zhàn)踩坑記錄:這些問題比代碼更重要
1. ODB 代碼生成遺漏導致鏈接錯誤
最開始用 ODB 的 ORM,只寫了user.hxx,沒生成 ORM 實現(xiàn)代碼,編譯時出現(xiàn)一堆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實現(xiàn)
)
2. Redis 客戶端初始化順序錯誤
最開始在make_redis_object里創(chuàng)建了Codes實例,但沒賦值給_codes_client,導致make_mock_sms_object時_codes_client是空的,運行崩潰。
解決方法:調整初始化順序,確保 Redis 客戶端先初始化,再創(chuàng)建Codes和MockSmsClient:
// UserServerBuilder
void make_redis_object(...) {
_redis_client = RedisClientFactory::create(...);
// 初始化Codes,賦值給成員變量
_codes_client = std::make_shared<Codes>(_redis_client);
}
void make_mock_sms_object() {
// 此時_codes_client已初始化,不會空指針
_mock_sms_client = std::make_shared<MockSmsClient>(_codes_client);
}
3. 依賴庫鏈接不全導致未定義錯誤
編譯時出現(xiàn)undefined reference to sw::redis::Redis::set,是因為 CMake 沒鏈接swredis++和hiredis庫。
解決方法:在 CMake 里添加鏈接:
# 查找依賴庫
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 個核心要點
無狀態(tài)設計:用戶服務不存本地數(shù)據(jù)(會話、狀態(tài)都放 Redis),方便橫向擴展,加節(jié)點就能扛更高并發(fā);
依賴注入優(yōu)先:所有外部依賴(短信、數(shù)據(jù)庫、緩存)都通過構造函數(shù)注入,方便替換和測試,比如用模擬短信突破平臺限制;
分層清晰:業(yè)務邏輯(注冊登錄)、數(shù)據(jù)訪問(ODB/Redis/ES)、服務調用(ServiceManager)分層,修改某一層不影響其他層。
做用戶服務時,最容易忽略的是 “可測試性” 和 “容錯性”—— 比如一開始沒做模擬短信,導致沒法本地測試手機號登錄;沒加重試機制,文件服務偶爾超時就失敗。這些問題都是實戰(zhàn)中踩出來的,比單純的代碼實現(xiàn)更有價值。
到此這篇關于C++微服務UserServer設計與實現(xiàn)方法的文章就介紹到這了,更多相關C++微服務UserServer實現(xiàn)內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

