4. 用户服务边界:用户注册、登录、资料管理服务的职责与边界定义
在社交平台微服务架构中,用户服务(User Service)是最基础、最核心的领域服务之一。它承载着用户身份的创建、认证与个人数据的维护职责。本章将深入剖析用户服务的内部职责划分、对外接口边界以及数据一致性保障策略,帮助架构师在服务拆分时做出合理决策。
4.1 用户服务的核心职责域
用户服务并非一个“大而全”的用户管理系统,而是围绕用户身份生命周期与基础档案管理两个核心维度进行职责收敛。其职责边界可归纳为以下三大模块:
| 模块 | 核心职责 | 典型操作 | 数据归属 |
|---|---|---|---|
| 用户注册 | 创建用户唯一身份标识,完成账号初始化 | 手机/邮箱验证、密码哈希、生成用户ID | 用户基础表(user_base) |
| 用户登录 | 验证用户凭证,颁发会话令牌 | 密码校验、OAuth2.0授权、Token签发 | 认证表(auth_credential) |
| 资料管理 | 维护用户可编辑的个人档案信息 | 昵称修改、头像上传、个人简介更新 | 用户档案表(user_profile) |
架构原则:用户服务只负责“你是谁”和“如何证明你是你”,不负责“你发了什么内容”、“你关注了谁”或“你的消息列表”。社交关系、内容发布、即时通讯等行为数据应归属其他领域服务。
4.2 服务边界与数据拆分策略
为了支撑高并发注册与登录场景,同时避免单表过大导致的性能瓶颈,用户服务在数据库层面应采用垂直拆分 + 逻辑分表策略。下图展示了用户服务的内部数据模型与外部依赖关系:
数据拆分建议:将用户基础信息(user_base)与用户档案信息(user_profile)拆分为两张独立表。基础表仅包含用户ID、注册手机/邮箱、密码哈希、账号状态等不可频繁变动的字段;档案表则存储昵称、头像、简介等可高频更新的字段。这样可减少写锁冲突,提升并发能力。
4.3 注册流程的职责边界
用户注册是用户服务的入口操作,其职责边界必须清晰:只负责创建用户身份,不负责后续的初始化流程(如发送欢迎消息、创建默认关注列表等)。注册流程应遵循以下步骤:
- 参数校验:验证手机号/邮箱格式、密码强度、验证码有效性。
- 唯一性检查:查询user_base表,确保手机号/邮箱未被注册。
- 密码哈希:使用bcrypt或argon2算法对密码进行加盐哈希,禁止明文存储。
- 用户ID生成:采用雪花算法(Snowflake)或分布式ID生成器,保证全局唯一且趋势递增。
- 数据写入:在同一个本地事务中,同时写入user_base和auth_credential表。
- 事件发布:向消息队列发送
UserRegistered事件,供其他服务消费。
// 用户注册核心逻辑(Go伪代码)
func (s *UserService) Register(ctx context.Context, req *RegisterRequest) (*RegisterResponse, error) {
// 1. 参数校验
if err := s.validateRegisterReq(req); err != nil {
return nil, err
}
// 2. 唯一性检查
exists, err := s.repo.CheckPhoneExists(ctx, req.Phone)
if err != nil || exists {
return nil, ErrPhoneAlreadyRegistered
}
// 3. 密码哈希
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 4. 生成用户ID(雪花算法)
userID := s.idGenerator.NextID()
// 5. 本地事务写入
err = s.repo.Transaction(ctx, func(tx *sql.Tx) error {
if err := tx.InsertUserBase(ctx, userID, req.Phone, req.Nickname); err != nil {
return err
}
return tx.InsertAuthCredential(ctx, userID, string(hashedPwd))
})
if err != nil {
return nil, err
}
// 6. 发布事件(异步)
s.eventBus.Publish(ctx, &UserRegisteredEvent{UserID: userID, Phone: req.Phone})
return &RegisterResponse{UserID: userID}, nil
}
边界陷阱:不要在注册流程中同步调用其他服务(如通知服务、推荐服务)。注册操作应保持高可用与低延迟,任何外部依赖的超时或失败都不应阻塞用户注册成功。后续的初始化操作应通过事件驱动异步完成。
4.4 登录流程的职责边界
登录模块的核心职责是验证用户身份并颁发会话凭证。它不负责记录登录日志(应由审计服务处理),也不负责判断用户是否被禁言(应由风控服务判断)。登录流程的典型实现如下:
| 步骤 | 操作 | 数据源 | 输出 |
|---|---|---|---|
| 1 | 根据手机号/邮箱查询用户 | user_base | 用户ID、账号状态 |
| 2 | 校验账号状态(是否冻结/删除) | user_base | 状态校验结果 |
| 3 | 读取密码哈希并比对 | auth_credential | 密码匹配结果 |
| 4 | 生成Access Token + Refresh Token | JWT / Redis | Token对 |
| 5 | 缓存用户基础信息到Redis | Redis | 用户基础信息缓存 |
// 用户登录核心逻辑(Go伪代码)
func (s *UserService) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// 1. 查询用户
user, err := s.repo.GetUserByPhone(ctx, req.Phone)
if err != nil || user == nil {
return nil, ErrUserNotFound
}
// 2. 校验账号状态
if user.Status != AccountStatusActive {
return nil, ErrAccountFrozen
}
// 3. 密码比对
credential, err := s.repo.GetCredentialByUserID(ctx, user.UserID)
if err != nil {
return nil, err
}
if err := bcrypt.CompareHashAndPassword([]byte(credential.PasswordHash), []byte(req.Password)); err != nil {
return nil, ErrPasswordMismatch
}
// 4. 生成Token
accessToken, refreshToken, err := s.tokenManager.GenerateTokenPair(user.UserID)
if err != nil {
return nil, err
}
// 5. 缓存用户信息
s.cache.SetUserBase(ctx, user.UserID, user)
return &LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: 7200,
}, nil
}
4.5 资料管理的职责边界
资料管理模块负责用户个人档案的读写操作,但需要严格区分用户可编辑字段与系统保留字段。用户可编辑字段包括:昵称、头像、个人简介、性别、生日、地区等。系统保留字段包括:注册时间、最后登录时间、账号等级、身份认证标识等。
设计要点:资料更新操作应发布
UserProfileUpdated事件,供搜索服务、推荐服务、好友服务等下游消费。但用户服务本身不关心这些下游如何处理事件,只保证事件至少投递一次(At-Least-Once)。
4.6 服务边界总结与演进方向
用户服务的边界定义应遵循高内聚、低耦合原则。在社交平台初期,用户服务可以作为一个单体服务运行;随着业务复杂度提升,可进一步拆分为账号服务(Account Service)和档案服务(Profile Service),前者专注认证安全,后者专注资料展示。但无论如何拆分,用户ID始终是跨服务通信的唯一标识。
架构决策参考:如果社交平台日活(DAU)低于100万,建议保持用户服务为单体,仅做逻辑分层。当DAU超过500万且资料更新QPS超过5000时,再考虑将档案服务独立拆分,以隔离读写负载。