3.注册流程

3.注册流程


3.1 注册流程图

graph TD
    subgraph 客户端请求鉴权服
        cli_click_register["点击“注册”按钮"]
        cli_select_auth_svr["选择鉴权服务器地址"]
        cli_create_session["创建网络会话"]
        cli_send_register_request["发送注册请求"]
    end

    subgraph 鉴权服处理
        svr_handle_register_request["处理注册请求"]
        svr_check_request_frequency["检查请求频率"]
        svr_set_session_timeout["设置会话超时时间"]
        svr_invoke_register_method["调用注册方法"]
        svr_validate_parameters["校验参数是否合法"]
        svr_validate_parameters_error["返回错误:校验参数非法"]
        svr_compute_svr_position["计算服务器位置"]
        svr_check_position_match{"判断是否应该在当前鉴权服务器中处理,即服务器位置是否匹配?"}
        svr_return_position_error["返回错误:账号不在此服务器处理"]
        svr_acquire_coroutine_lock["获取协程锁"]
        svr_check_account_cache{"账号存在于缓存?"}
        svr_return_account_exists_error["返回错误:用户已存在"]
        svr_query_database["查询数据库"]
        svr_check_account_exists{"账号存在?"}
        svr_create_new_account["创建新账号"]
        svr_save_account["保存账号"]
        svr_cache_account_with_timeout["将账号添加到缓存,并设置账号缓存超时时间,超时清除缓存"]
        svr_return_success["返回成功"]
    end

    subgraph 客户端接收鉴权服响应
      cli_receive_response["接收注册响应"]
      cli_receive_check_error{"检查错误码?"}
      cli_register_success["注册成功"]
      cli_register_fail["注册失败"]
    end

    cli_click_register --> cli_select_auth_svr --> cli_create_session --> cli_send_register_request
    cli_send_register_request --> svr_handle_register_request --> svr_check_request_frequency --> svr_set_session_timeout --> svr_invoke_register_method --> svr_validate_parameters
    
    svr_validate_parameters -- 否 --> svr_validate_parameters_error--> cli_receive_response
    svr_validate_parameters -- 是 --> svr_compute_svr_position

    svr_compute_svr_position --> svr_check_position_match
    svr_check_position_match -- 否 --> svr_return_position_error--> cli_receive_response
    svr_check_position_match -- 是 --> svr_acquire_coroutine_lock --> svr_check_account_cache
    svr_check_account_cache -- 是 --> svr_return_account_exists_error--> cli_receive_response
    svr_check_account_cache -- 否 --> svr_query_database --> svr_check_account_exists
    svr_check_account_exists -- 是 --> svr_return_account_exists_error
    svr_check_account_exists -- 否 --> svr_create_new_account --> svr_save_account --> svr_cache_account_with_timeout --> svr_return_success
    svr_return_success --> cli_receive_response --> cli_receive_check_error
    cli_receive_check_error -- 有错误 --> cli_register_fail
    cli_receive_check_error -- 无错误 --> cli_register_success

3.2 注册时客户端应该具备的能力

客户端应该具备选择鉴权服务器地址的能力,根据用户名找到用户应该请求的鉴权服务器,保证负载均衡

// 添加认证服务器选择组件(一致性哈希算法选择服务器)
_scene.AddComponent<AuthenticationSelectComponent>();

3.3 选择鉴权服务器地址

商业级注册登录系统会选择多鉴权服务器。客户端应该具备选择鉴权服务器地址的能力,以便在注册和登录时找到正确的鉴权服务器发起请求。

单鉴权的问题

由于负载有限,一旦出现问题,整个登录鉴权功能可能完全瘫痪,导致用户无法登录系统并影响服务的正常运行。
这种情况通常是因为单点瓶颈,导致过多请求涌入单个节点而超出其承载能力,最终引发系统不可用的情况。

多鉴权

去中心化

  • 实现方式:让各个节点独立维护一致性哈希环
    在分布式系统中,一种常见的实现是每个服务器节点自己维护一个一致性哈希环的副本。每个节点都知道整个系统中的其他节点和哈希槽的分布情况,这样每个请求就可以直接路由到目标节点,而不需要通过单点服务器来转发。

  • 优点
    消除了单点瓶颈,降低了服务器负载,并提高了系统的容错性。

  • 缺点
    需要设计一种机制来在节点增减时同步各个节点的哈希环副本。

分布式哈希表(DHT)

  • 实现方式
    类似于 P2P 系统中使用的分布式哈希表,可以避免单点故障。每个节点只存储其直接相邻的部分信息,这样就能逐跳路由请求到正确的节点。
    分布式哈希表是一种去中心化的解决方案,在大规模系统中非常有效。

客户端直接实现一致性哈希算法

  • 实现方式
    一致性哈希槽由客户端实现。客户端在初始化时获取所有服务器节点的列表,并使用一致性哈希算法自行选择适当的服务器节点。

  • 优点
    减少了服务器负载,消除了中央管理节点,客户端可以直接访问目标节点。

  • 缺点
    当节点动态变动时,需要通知所有客户端更新节点列表,否则可能会导致请求失败或不一致。

一致性哈希算法

BKDR

  • 概述
    BKDR Hash 是一种较为简单的哈希算法,常用于字符串哈希。通过选取一个合适的“种子”来混合输入字符串中的每个字符,产生最终的哈希值。

  • 优点

    • 简单易实现:算法逻辑简单,代码实现短小,适合轻量级应用。
    • 适合短字符串:对于短字符串,生成哈希值的速度较快。
  • 缺点

    • 碰撞概率较高:由于其简单的线性乘法结构,在处理较长数据或不同数据时,哈希碰撞的概率会明显增加,特别是在处理大量数据时。
    • 不均匀性:对于某些种子值,哈希值分布不够均匀,可能导致哈希表中出现“热点”问题。

MurmurHash3算法

  • 概述
    MurmurHash3 是由 Austin Appleby 提出的哈希函数,专为高性能场景设计,广泛应用于分布式哈希、哈希表和数据库等系统中。该算法以其速度快和散列分布良好而著称。

  • 优点

    • 高速:MurmurHash3 针对 CPU 指令集优化,能在大多数处理器上非常高效地执行。
    • 良好的散列分布:在大量数据上分布均匀,避免哈希碰撞。
    • 平台无关性:对不同平台具有良好的兼容性。
  • 缺点

    • 非加密哈希:没有强加密性质,不能用于安全相关的哈希需求(如加密签名)。
    • 不抗攻击:在恶意输入下,可能导致哈希碰撞增多,进而影响性能。

代码示例

/// <summary>
/// 认证服务器选择组件扩展系统
/// 实现基于一致性哈希的服务器选择逻辑
/// </summary>
public static class AuthenticationSelectComponentSystem
{
    // 预定义的认证服务器地址列表(本地开发环境配置)
    private static readonly List<string> AuthenticationList = new List<string>()
    {
        "127.0.0.1:20001", // 认证服务器节点1
        "127.0.0.1:20002", // 认证服务器节点2
        "127.0.0.1:20003" // 认证服务器节点3
    };

    /// <summary>
    /// 根据用户名选择目标认证服务器地址
    /// </summary>
    /// <param name="self">组件实例</param>
    /// <param name="userName">用户名</param>
    /// <returns>选中的认证服务器地址</returns>
    public static string Select(this AuthenticationSelectComponent self, string userName)
    {
        // 使用MurmurHash3算法计算用户名的哈希值
        // 特点:高随机性、低碰撞率,适合分布式系统哈希计算
        var userNameHashCode = HashCodeHelper.MurmurHash3(userName);

        // 通过取模运算确定目标服务器索引
        // 计算逻辑:哈希值 % 服务器总数(结果范围:0 到 AuthenticationList.Count-1)
        var authenticationListIndex = userNameHashCode % AuthenticationList.Count;
        // 按照现在的情况下,这个模出的值只会是0 - 3
        return AuthenticationList[(int)authenticationListIndex];
    }
}

3.4 注册时鉴权服务器应该具备的能力

鉴权服务器应该具备处理注册逻辑的能力

// 用于鉴权服务器注册和登录相关逻辑的组件
scene.AddComponent<AuthenticationComponent>().UpdatePosition();

3.5 鉴权服务器初始化

鉴权服务器初始化时要更新当前鉴权服务器在所有鉴权组中的位置和总数。简单来说是得知道自己是几号鉴权服务器。

// 更新当前鉴权服务器在所有鉴权组中的位置和总数
public static void UpdatePosition(this AuthenticationComponent self)
{
    // 1、通过远程接口或者本地文件来拿到所有鉴权服务器的配置信息,这里通过 SceneConfigData 获取
    var authentications = SceneConfigData.Instance.GetSceneBySceneType(SceneType.Authentication);
    
    // 2、获取当前 Scene 对应的配置文件
    var sceneConfig = SceneConfigData.Instance.Get(self.Scene.SceneConfigId);
    
    // 3、在鉴权服务器组中找到当前 Scene 配置的位置索引
    self.Position = authentications.IndexOf(sceneConfig);
    
    // 4、设置鉴权服务器的总数量
    self.AuthenticationCount = authentications.Count;
    
    // 输出日志,记录当前鉴权服务器的位置和总数
    Log.Info($"鉴权服务器启动成功!Position:{self.Position} AuthenticationCount:{self.AuthenticationCount}");
}

3.6 鉴权服务器收到注册请求时的前置准备及检查

检查请求频率

设置每个Session每次接收请求的间隔时间,小于这个时间不予处理,统一作为恶意链接处理

// 检查客户端请求间隔是否符合要求,以防止用户发送过于频繁的请求
if (!session.CheckInterval(2000))
{
    // 如果请求过于频繁,则将错误码设置为3,表示操作过于频繁,然后直接返回
    response.ErrorCode = 3;
    return;
}

设置会话超时时间

给Session会话设置超时时间,到指定时间自动断开Session

// 为当前会话设置一个超时时间,确保请求在规定时间内完成,否则断开Session
session.SetTimeout(3000);

校验注册所需参数是否合法

检查注册所需的用户名和密码是否为空,防止不完整的注册请求

if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
    // 返回错误码1,表示参数不完整
    return 1;
}

判断当前账号请求是否应该由当前鉴权服务器处理

var position = HashCodeHelper.MurmurHash3(username) % self.AuthenticationCount;
if (self.Position != position)
{
    // 返回错误码3,表示该账号不应在当前服务器上进行注册操作
    return 3;
}

3.7 注册时高并发时原子逻辑

获取用户名的哈希码作为协程锁的键值,防止高并发时多个相同用户名的用户多个注册请求,导致数据混乱

// 获取用户名的哈希码,用于协程锁的键值,防止多个注册请求并发导致数据混乱
var usernameHashCode = username.GetHashCode();
var scene = self.Scene;

// 使用协程锁确保同一用户名的注册请求是串行执行的
using (var @lock =
        await scene.CoroutineLockComponent.Wait((int)LockType.AuthenticationRegisterLock, usernameHashCode))
{
    //...
}

3.7 注册核心逻辑

  1. 首先检查缓存,若存在则不应该注册。
  2. 缓存找不到再查数据库,数据库也没有代表要注册新用户并存库了。
  3. 创建完添加新创建的用户到缓存并设置超时时间,过期时清除过期的缓存信息来减少内存压力。
// 使用协程锁确保同一用户名的注册请求是串行执行的
using (var @lock =
        await scene.CoroutineLockComponent.Wait((int)LockType.AuthenticationRegisterLock, usernameHashCode))
{
    // 首先通过缓存判断该用户名是否已经存在,减少对数据库的访问次数
    if (self.RegisterAccountCacheDic.TryGetValue(username, out var account))
    {
        // 返回错误码2,表示该用户已经存在
        return 2;
    }

    // 如果缓存中没有找到,再查询数据库判断账号是否存在
    var worldDateBase = scene.World.DateBase;
    var isExist = await worldDateBase.Exist<Account>(d => d.Username == username);
    if (isExist)
    {
        // 数据库中存在相同用户名,则返回错误码2,表示账号已存在
        return 2;
    }

    // 执行到此处说明缓存和数据库中都没有该账号信息,需要创建新账号
    account = Entity.Create<Account>(scene, true, true);
    // 将注册信息写入新创建的账号实体
    account.Username = username;
    account.Password = password;
    account.CreateTime = TimeHelper.Now;

    // 将新账号保存到数据库中,持久化存储
    await worldDateBase.Save(account);
    var accountId = account.Id;

    // 将新账号添加到缓存字典中,便于后续快速登录等操作
    self.RegisterAccountCacheDic.Add(username, account);

    // 为账号添加超时组件,设置缓存失效时间为4000毫秒,自动清理过期账号缓存
    account.AddComponent<AccountTimeOut>().TimeOut(4000);

    // 记录注册日志,输出注册来源、用户名及账号ID,便于系统运维和问题追踪
    Log.Info($"Register source:{source} username:{username} accountId:{accountId}");

    // 返回0表示注册成功
    return 0;
}

Account一般定义如下

public sealed class Account : Entity
{
    public string Username { get; set; }
    public string Password { get; set; }
    public long CreateTime { get; set; }
    public long LoginTime { get; set; }
}

3.9 客户端受到注册请求的响应

基于错误码判断是否注册成功

// 发送一个注册的请求消息到目标服务器
var response = (A2C_RegisterResponse)await _session.Call(new C2A_RegisterRequest()
{
    Username = UsernameInput.text,
    Password = PasswordInput.text
});

// 处理注册结果
if (response.ErrorCode != 0)
{
    Log.Error($"注册失败,错误码: {response.ErrorCode}");
    return;
}
Log.Debug("注册成功!");

3.10 总结

注册流程是用户进入系统的第一道门槛,其设计需要兼顾高并发、高可用、安全性数据一致性。以下是本文核心要点总结:

核心流程

  • 客户端能力:通过一致性哈希算法(如MurmurHash3)选择鉴权服务器实现负载均衡,封装用户名/密码参数并发送注册请求。
  • 鉴权服务器处理
    • 前置校验:拦截高频请求,设置3000ms会话超时,校验用户名/密码非空及格式合法性。
    • 服务器位置验证:通过用户名哈希计算匹配目标服务器,拒绝非指定服务器请求。
    • 原子操作:利用协程锁防止高并发下重复注册,确保操作唯一性。
    • 缓存与数据库联动:先查注册缓存(4000ms超时),未命中则查询数据库,新账号写入后同步缓存。

关键设计

  • 多鉴权服务器架构
    • 通过一致性哈希(如 MurmurHash3)分配请求,避免单点瓶颈。
    • 动态管理服务器列表(如配置中心或服务发现),支持横向扩展。
  • 高并发处理
    • 协程锁:防止同一用户名并发注册导致脏数据。
    • 缓存超时机制:设置合理的缓存失效时间(如 4 秒),平衡性能与数据一致性。
  • 安全性
    • 参数合法性检查:拦截非法请求(如空值、超长用户名)。
    • 密码安全存储:推荐使用加盐哈希(如 BCrypt),避免明文存储。

优化方向

  • 动态服务器列表:通过服务发现(如 Consul、ETCD)动态管理鉴权服务器节点。
  • 增强校验规则:引入密码复杂度检查(如大小写字母、数字组合)、用户名黑名单过滤。
  • 容错机制
    • 缓存穿透防护:使用布隆过滤器(Bloom Filter)拦截无效查询。
    • 超时动态调整:根据历史请求延迟自适应设置会话超时时间。

实际应用建议

  • 错误码标准化:使用枚举类型(如 ErrorCode.UserExists)替代数字,提升代码可读性。
  • 日志与监控:记录关键操作(如注册来源、账号 ID),结合 Prometheus 监控请求延迟和错误率。
  • 自动化测试:模拟高并发注册场景(如 JMeter 压测),验证系统健壮性。

通过以上设计,注册流程既能支撑大规模用户并发请求,又能保障数据安全性与系统稳定性,为后续登录、鉴权等环节奠定坚实基础。



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏