多人在线游戏架构实战第10章:分布式登录与Redis内存数据库——水平扩展的钥匙

📑 目录

第10章:分布式登录与Redis内存数据库——水平扩展的钥匙

本章一句话总结:Redis不是缓存,它是分布式游戏服务器的"神经系统"——所有进程共享状态、协调动作、支撑无状态服务的基石。

一、从单进程到分布式:登录架构的重新思考

在第9章我们聊过gamespace进程的职责分离。但分布式架构带来了一个根本性问题:玩家的登录状态,应该存在哪里?

1.1 单进程时代的登录状态

// 单进程时代:登录状态存在内存里,简单粗暴
class LoginManager {
    std::unordered_map<uint64_t, Account*> onlineAccounts_;
    
public:
    Account* GetAccount(uint64_t accountId) {
        return onlineAccounts_[accountId];  // 直接内存指针
    }
};

这很好,直到你需要开第二个进程。两个进程的内存互不相通,玩家在A进程登录后,B进程根本不知道这回事。

1.2 分布式时代的核心需求

需求单进程方案分布式方案
账号状态共享进程内map共享存储(Redis)
进程宕机恢复玩家全部掉线另一进程接管,玩家无感知
动态扩缩容没法扩新进程启动即参与负载均衡
登录信息查询遍历进程内存全集群统一查询
graph LR
    A[客户端] -- 连接 --> B[Game进程1]
    A -- 连接 --> C[Game进程2]
    A -- 连接 --> D[Game进程3]
    
    B -- 读写 --> E[Redis集群]
    C -- 读写 --> E
    D -- 读写 --> E
    
    E -- 状态共享 --> B
    E -- 状态共享 --> C
    E -- 状态共享 --> D

二、选择合适的game进程:负载均衡的入门课

2.1 玩家应该连到哪个game进程?

有四种常见的策略,从蠢到聪明:

轮询(Round Robin):每个新连接轮流分配给game进程。简单,但不考虑进程当前负载。

随机(Random):随机选一个。比轮询更差,因为随机不保证均匀。

最小连接数(Least Connections):选当前在线玩家最少的进程。好得多,但假设每个玩家的负载相同。

自定义负载分(Custom Score):综合CPU、内存、网络IO、当前在线数算一个分数,选分最低的。

struct GameProcessLoad {
    uint32_t processId;
    uint32_t currentPlayers;     // 当前在线玩家数
    float cpuPercent;            // CPU占用(0.0-1.0)
    uint64_t memoryBytes;        // 内存占用
    uint64_t networkInBytes;     // 网络入流量
    uint64_t networkOutBytes;    // 网络出流量
    time_t lastReportTime;       // 上次上报时间(过期则不考虑)
    
    // 综合负载分数,越小越空闲
    double GetLoadScore() const {
        // 权重可调,根据实际业务调整
        constexpr double W_PLAYER = 1.0;
        constexpr double W_CPU = 1000.0;     // CPU打满=1000个玩家
        constexpr double W_MEM = 0.001;       // 每MB内存
        constexpr double W_NET = 0.0001;      // 每KB网络
        
        return currentPlayers * W_PLAYER
             + cpuPercent * 100 * W_CPU
             + memoryBytes / 1024 / 1024 * W_MEM
             + (networkInBytes + networkOutBytes) / 1024 * W_NET;
    }
};

// 选择最优进程
uint32_t SelectBestGameProcess(const std::vector<GameProcessLoad>& processes) {
    auto best = std::min_element(processes.begin(), processes.end(),
        [](const auto& a, const auto& b) {
            return a.GetLoadScore() < b.GetLoadScore();
        });
    return best->processId;
}

2.2 Redis存储负载信息

// 每个game进程定期上报自己的负载(每5秒)
void ReportLoadToRedis(RedisConnector* redis, const GameProcessLoad& load) {
    // 使用Hash存储:key = "game:load:{processId}"
    redis->HMSet(fmt::format("game:load:{}", load.processId), {
        {"players", std::to_string(load.currentPlayers)},
        {"cpu", fmt::format("{:.2f}", load.cpuPercent)},
        {"memory", std::to_string(load.memoryBytes)},
        {"last_report", std::to_string(load.lastReportTime)}
    });
    
    // 设置过期时间:15秒,超过则认为进程已宕机
    redis->Expire(fmt::format("game:load:{}", load.processId), 15);
}

// 查询所有存活的game进程
std::vector<GameProcessLoad> QueryAliveGameProcesses(RedisConnector* redis) {
    std::vector<GameProcessLoad> result;
    
    // 使用Redis的SCAN或Keys查找所有game:load:*
    auto keys = redis->Keys("game:load:*");
    for (const auto& key : keys) {
        auto data = redis->HGetAll(key);
        if (data.empty()) continue;
        
        GameProcessLoad load;
        load.processId = ParseProcessId(key);
        load.currentPlayers = std::stoul(data["players"]);
        load.cpuPercent = std::stof(data["cpu"]);
        load.memoryBytes = std::stoull(data["memory"]);
        load.lastReportTime = std::stoll(data["last_report"]);
        
        // 只考虑最近10秒内上报的进程
        if (Now() - load.lastReportTime < 10) {
            result.push_back(load);
        }
    }
    
    return result;
}

三、使用token登录game:无状态服务的核心

3.1 为什么token是分布式登录的钥匙?

Cookie和Session是Web开发的经典组合,但在游戏服务器里,有状态=无法扩展。如果session存在某个game进程的内存里,那玩家下次请求就必须落到同一个进程——这叫粘性会话(Sticky Session),严重制约水平扩展。

Token机制的本质:服务器不存会话状态,所有状态都在token里,或者token能索引到的共享存储里。

3.2 token的生命周期管理

class TokenManager {
public:
    // 登录成功后生成token
    std::string GenerateToken(uint64_t accountId) {
        std::string token = GenerateRandomString(32);  // 32字节随机串
        
        // token -> 账号ID映射,存入Redis,TTL = 24小时
        redis_>SetEx(fmt::format("token:{}", token), 
                      std::to_string(accountId), 
                      24 * 3600);
        
        // 账号ID -> token映射,用于单点登录(后登录踢掉前登录)
        auto oldToken = redis_>Get(fmt::format("account_token:{}", accountId));
        if (!oldToken.empty()) {
            // 踢掉旧token
            redis_>Del(fmt::format("token:{}", oldToken));
        }
        redis_>SetEx(fmt::format("account_token:{}", accountId), 
                       token, 24 * 3600);
        
        return token;
    }
    
    // 验证token,返回账号ID
    std::optional<uint64_t> ValidateToken(const std::string& token) {
        auto accountIdStr = redis_>Get(fmt::format("token:{}", token));
        if (accountIdStr.empty()) {
            return std::nullopt;  // token不存在或已过期
        }
        
        // 续期:活跃玩家延长token有效期
        redis_>Expire(fmt::format("token:{}", token), 24 * 3600);
        
        return std::stoull(accountIdStr);
    }
    
    // 登出时销毁token
    void RevokeToken(const std::string& token) {
        auto accountId = redis_>Get(fmt::format("token:{}", token));
        if (!accountId.empty()) {
            redis_>Del(fmt::format("account_token:{}", accountId));
        }
        redis_>Del(fmt::format("token:{}", token));
    }
};

坑点预警:token续期策略要慎重。如果每次请求都续期,Redis的写入压力会很大。可以改为"距离过期还有1小时才续期",减少不必要的写入。


四、Player组件设计:面向组件的游戏对象

4.1 组件化 vs 继承

// 继承方式:一个巨大的Player类
class BadPlayer {
    // 基础属性
    uint64_t playerId;
    std::string name;
    
    // 战斗相关
    uint32_t hp, maxHp, mp, maxMp;
    uint32_t attack, defense;
    std::vector<Skill> skills;
    
    // 背包相关
    std::vector<Item> bag;
    uint32_t bagCapacity;
    
    // 社交相关
    std::vector<uint64_t> friends;
    std::vector<uint64_t> guildMembers;
    
    // 任务相关
    std::vector<Quest> activeQuests;
    std::vector<uint32_t> completedQuests;
    
    // ... 还有几十个字段
};

这种写法的问题是:所有玩家都拖着全部字段,但很多字段只在特定时刻使用。一个刚创建的角色,背包是空的,但你已经为它分配了std::vector的内存。

4.2 组件化设计

// 纯数据组件,没有行为
struct TransformComponent {
    Vector3 position;
    Vector3 rotation;
    Vector3 scale;
};

struct HealthComponent {
    uint32_t hp;
    uint32_t maxHp;
    uint32_t mp;
    uint32_t maxMp;
};

struct BagComponent {
    std::unordered_map<uint32_t, Item> items;  // slot -> item
    uint32_t capacity;
};

struct SkillComponent {
    std::vector<Skill> skills;
    std::vector<cooldown_t> cooldowns;  // 技能冷却
};

// Player是一个ID + 组件容器
class Player {
    uint64_t playerId_;
    std::unordered_map<std::type_index, std::shared_ptr<void>> components_;
    
public:
    template<typename T>
    void AddComponent(std::shared_ptr<T> comp) {
        components_[typeid(T)] = comp;
    }
    
    template<typename T>
    std::shared_ptr<T> GetComponent() const {
        auto it = components_.find(typeid(T));
        if (it == components_.end()) return nullptr;
        return std::static_pointer_cast<T>(it->second);
    }
    
    template<typename T>
    bool HasComponent() const {
        return components_.count(typeid(T)) > 0;
    }
};

// 使用
auto player = std::make_shared<Player>(10001);
player->AddComponent(std::make_shared<TransformComponent>());
player->AddComponent(std::make_shared<HealthComponent>(100, 100, 50, 50));

// 战斗系统只关心有HealthComponent的实体
if (auto health = player->GetComponent<HealthComponent>()) {
    health->hp -= damage;
}

坑点预警std::type_index做key的unordered_map有性能损耗。如果组件类型是固定的,可以用枚举ID代替,或者预计算type_index的哈希缓存。


五、Redis安装与C++接入

5.1 Redis安装(生产环境配置)

# 安装Redis(以Ubuntu为例)
sudo apt-get update
sudo apt-get install redis-server

# 编辑配置文件
sudo vim /etc/redis/redis.conf

# 关键配置项:
# bind 0.0.0.0          # 监听所有接口(生产环境建议绑定内网IP)
# port 6379             # 默认端口
# maxmemory 2gb         # 最大内存限制
# maxmemory-policy allkeys-lru  # 内存满时淘汰策略:LRU
# save ""               # 关闭RDB持久化(纯缓存场景)
# appendonly no         # 关闭AOF(纯缓存场景)
# tcp-keepalive 60      # TCP keepalive

# 启动
sudo systemctl start redis-server
sudo systemctl enable redis-server

# 验证
redis-cli ping
# 应返回 PONG

5.2 hiredis库的使用

hiredis是Redis官方C客户端,轻量、高效。

#include <hiredis/hiredis.h>

// 基础连接
class RedisConnection {
    redisContext* ctx_;
    
public:
    bool Connect(const std::string& ip, int port, int timeoutMs = 5000) {
        struct timeval tv = {timeoutMs / 1000, (timeoutMs % 1000) * 1000};
        ctx_ = redisConnectWithTimeout(ip.c_str(), port, tv);
        
        if (ctx_ == nullptr || ctx_>err) {
            fprintf(stderr, "Redis连接失败: %s\n", 
                    ctx_ ? ctx_>errstr : "无法分配内存");
            return false;
        }
        
        // 可选:认证
        // auto reply = (redisReply*)redisCommand(ctx_, "AUTH %s", password);
        // freeReplyObject(reply);
        
        return true;
    }
    
    // 基础命令
    std::string Get(const std::string& key) {
        auto reply = (redisReply*)redisCommand(ctx_, "GET %s", key.c_str());
        if (reply == nullptr) return "";
        
        std::string result;
        if (reply->type == REDIS_REPLY_STRING) {
            result = std::string(reply->str, reply->len);
        }
        
        freeReplyObject(reply);
        return result;
    }
    
    bool SetEx(const std::string& key, const std::string& value, int seconds) {
        auto reply = (redisReply*)redisCommand(ctx_, "SETEX %s %d %s", 
                                               key.c_str(), seconds, value.c_str());
        if (reply == nullptr) return false;
        bool ok = (reply->type == REDIS_REPLY_STATUS && 
                   std::string(reply->str) == "OK");
        freeReplyObject(reply);
        return ok;
    }
    
    ~RedisConnection() {
        if (ctx_) redisFree(ctx_);
    }
};

5.3 生产级RedisConnector组件

class RedisConnector {
public:
    // 初始化连接池
    bool Initialize(const std::string& ip, int port, 
                    int poolSize, int timeoutMs) {
        for (int i = 0; i < poolSize; ++i) {
            auto conn = std::make_unique<RedisConnection>();
            if (!conn->Connect(ip, port, timeoutMs)) {
                return false;
            }
            pool_.push(std::move(conn));
        }
        poolSize_ = poolSize;
        return true;
    }
    
    // 获取连接(线程安全)
    std::unique_ptr<RedisConnection> Acquire() {
        std::unique_lock<std::mutex> lock(mutex_);
        
        // 等待可用连接
        cv_.wait(lock, [this]() { return !pool_.empty() || shutdown_; });
        
        if (shutdown_) return nullptr;
        
        auto conn = std::move(pool_.front());
        pool_.pop();
        return conn;
    }
    
    // 归还连接
    void Release(std::unique_ptr<RedisConnection> conn) {
        std::lock_guard<std::mutex> lock(mutex_);
        pool_.push(std::move(conn));
        cv_.notify_one();
    }
    
    // 便捷:自动归还的包装
    template<typename Func>
    auto Execute(Func&& func) -> decltype(func(std::declval<RedisConnection*>())) {
        auto conn = Acquire();
        if (!conn) {
            throw std::runtime_error("Redis连接池已关闭");
        }
        
        try {
            auto result = func(conn.get());
            Release(std::move(conn));
            return result;
        } catch (...) {
            Release(std::move(conn));
            throw;
        }
    }
    
private:
    std::queue<std::unique_ptr<RedisConnection>> pool_;
    std::mutex mutex_;
    std::condition_variable cv_;
    bool shutdown_ = false;
    int poolSize_ = 0;
};

// 使用示例
RedisConnector redis;
redis.Initialize("127.0.0.1", 6379, 10, 5000);

// 自动管理连接生命周期
auto result = redis.Execute([](RedisConnection* conn) {
    return conn->Get("token:abc123");
});

六、Redis在login和game中的应用全景

6.1 登录流程中的Redis操作

sequenceDiagram
    participant C as 客户端
    participant G as Game进程
    participant R as Redis
    participant DB as 数据库

    C->>G: 连接请求
    G->>R: 检查IP限流(INCR ip:{ip} + EXPIRE)
    R-->>G: 未超限
    
    C->>G: 发送账号密码
    G->>DB: 验证账号密码
    DB-->>G: 验证成功
    
    G->>G: 生成token
    G->>R: SETEX token:{token} {accountId} 86400
    G->>R: SETEX account_token:{accountId} {token} 86400
    G->>R: HSET player:{accountId} ...(加载角色数据到Redis缓存)
    
    G-->>C: 返回token
    
    Note over C,R: 后续请求
    C->>G: 请求(携带token)
    G->>R: GET token:{token}
    R-->>G: accountId
    G->>R: HGETALL player:{accountId}(热数据)
    G-->>C: 响应

6.2 Redis数据结构选型指南

用途Redis数据结构示例
token存储StringSETEX token:xxx 86400 10001
玩家在线状态HashHSET player:10001 hp 100 mp 50
排行榜Sorted SetZADD rank:level 100 player:10001
好友列表SetSADD friends:10001 10002 10003
聊天频道ListLPUSH chat:world "msg"
限流计数String + INCRINCR ip:1.2.3.4
分布式锁String + NXSET lock:xxx my_value NX EX 10
配置热更新HashHSET config:game max_players 5000

七、数据删除策略:Redis不是保险箱

7.1 为什么需要主动清理?

Redis的内存是宝贵的。一个运营了3年的游戏,Redis里可能堆积了:已删号玩家的残留数据、测试服误写入的脏数据、过期但未被访问的key(如果用了惰性过期策略)。

7.2 分级删除策略

class DataCleanupManager {
public:
    // 策略1:登录时清理旧数据
    void OnPlayerLogin(uint64_t accountId) {
        // 检查账号是否已被注销
        auto status = redis_>HGet(fmt::format("account:{}", accountId), "status");
        if (status == "deleted") {
            // 拒绝登录,并触发异步清理
            ScheduleCleanup(accountId);
            return;
        }
    }
    
    // 策略2:定时扫描过期数据(每晚3点执行)
    void NightlyCleanup() {
        // 扫描所有带TTL的key
        auto cursor = 0;
        do {
            auto reply = redis_>Scan(cursor, "player:*", 1000);
            cursor = reply.cursor;
            
            for (const auto& key : reply.keys) {
                // 检查对应的账号是否还在线
                auto accountId = ExtractAccountId(key);
                if (!IsPlayerOnline(accountId)) {
                    // 不在线且数据超过7天未更新则删除
                    auto lastUpdate = redis_>ObjectIdleTime(key);
                    if (lastUpdate > 7 * 86400) {
                        redis_>Del(key);
                    }
                }
            }
        } while (cursor != 0);
    }
    
    // 策略3:账号注销时的级联删除
    void OnAccountDeleted(uint64_t accountId) {
        // 使用管道批量删除,减少RTT
        auto pipe = redis_>Pipeline();
        pipe->Del(fmt::format("token:{}", GetToken(accountId)));
        pipe->Del(fmt::format("account_token:{}", accountId));
        pipe->Del(fmt::format("player:{}", accountId));
        pipe->Del(fmt::format("bag:{}", accountId));
        pipe->Del(fmt::format("friends:{}", accountId));
        pipe->Execute();
    }
};

八、性能瓶颈分析与优化

8.1 日志:性能杀手的第一嫌疑人

// 危险!每个请求都写日志
void OnMoveRequest(const MoveReq& req) {
    LOG_INFO("玩家{}从({}, {})移动到({}, {})", 
             req.playerId, req.fromX, req.fromY, req.toX, req.toY);
    // ... 处理移动
}

// 优化1:日志级别控制
void OnMoveRequest(const MoveReq& req) {
    // 使用宏,DEBUG级别在Release编译时直接消失
    DEBUG_LOG("玩家{}移动", req.playerId);
    // ... 处理移动
}

// 优化2:异步日志
class AsyncLogger {
    std::queue<std::string> buffer_;
    std::thread writerThread_;
    
    void WriterLoop() {
        while (running_) {
            std::vector<std::string> batch;
            {
                std::lock_guard<std::mutex> lock(mutex_);
                batch.reserve(buffer_.size());
                while (!buffer_.empty()) {
                    batch.push_back(std::move(buffer_.front()));
                    buffer_.pop();
                }
            }
            
            // 批量写入文件
            FILE* fp = fopen(logFile_.c_str(), "a");
            for (const auto& msg : batch) {
                fwrite(msg.c_str(), 1, msg.size(), fp);
            }
            fclose(fp);
            
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    }
};

8.2 MessageComponent:网络层的内存优化

// 原始:每个消息都动态分配内存
class BadMessageComponent {
    std::queue<std::vector<char>> sendQueue_;
    
    void Send(const void* data, size_t len) {
        std::vector<char> buffer(data, data + len);  // 堆分配
        sendQueue_.push(std::move(buffer));
    }
};

// 优化:预分配内存池 + 环形缓冲区
class OptimizedMessageComponent {
    static constexpr size_t BLOCK_SIZE = 65536;  // 64KB
    static constexpr size_t BLOCK_COUNT = 1024;  // 总共64MB
    
    struct MemoryBlock {
        char data[BLOCK_SIZE];
        size_t used = 0;
        bool inUse = false;
    };
    
    std::array<MemoryBlock, BLOCK_COUNT> pool_;
    std::queue<MemoryBlock*> freeList_;
    std::mutex poolMutex_;
    
public:
    OptimizedMessageComponent() {
        for (auto& block : pool_) {
            freeList_.push(&block);
        }
    }
    
    MemoryBlock* AcquireBlock() {
        std::lock_guard<std::mutex> lock(poolMutex_);
        if (freeList_.empty()) {
            // 池满了,要么扩容,要么等
            return nullptr;
        }
        auto* block = freeList_.front();
        freeList_.pop();
        block->inUse = true;
        block->used = 0;
        return block;
    }
    
    void ReleaseBlock(MemoryBlock* block) {
        std::lock_guard<std::mutex> lock(poolMutex_);
        block->inUse = false;
        freeList_.push(block);
    }
};

8.3 ConnectObj:连接对象的内存组织

// 原始:每个连接都存完整玩家数据
class HeavyConnectObj {
    Account account_;           // 账号信息
    Player player_;             // 角色信息(可能很大)
    BagComponent bag_;          // 背包
    SkillComponent skills_;     // 技能
    // ... 所有组件都存这里
};

// 优化:延迟加载 + 引用计数
class LightConnectObj {
    uint64_t accountId_;
    uint64_t playerId_;
    NetworkConnect* conn_;
    
    // 游戏数据不在连接对象里,而在全局缓存中
    // 需要时从Redis或本地缓存获取
    std::shared_ptr<PlayerData> GetPlayerData() {
        return PlayerDataCache::Instance().Get(playerId_);
    }
};

九、多进程登录协议完整回顾

sequenceDiagram
    participant C as 客户端
    participant L as Login/Game进程
    participant R as Redis
    participant S as Space进程
    participant DB as 数据库

    %% 连接阶段
    C->>L: TCP连接
    L->>R: INCR connect_limit:{ip}
    alt 连接数超限
        L-->>C: 断开连接
    else 未超限
        %% 登录阶段
        C->>L: LoginReq(account, password)
        L->>DB: 验证账号密码
        DB-->>L: 成功
        
        L->>L: 生成token
        L->>R: SETEX token:{token} {accountId} TTL
        L->>R: SETEX account_token:{accountId} {token} TTL
        
        L->>R: HGET player:{accountId}(检查角色)
        alt 无角色
            L-->>C: 进入创角流程
            C->>L: CreateRoleReq(name, ...)
            L->>DB: 写入新角色
            L->>R: HSET player:{accountId} ...
        end
        
        L->>R: 加载角色数据到Redis缓存
        L->>R: SADD online_players {accountId}
        
        L-->>C: LoginRes(success, token, playerData)
        
        %% 进入游戏阶段
        C->>L: EnterGameReq(token, mapId)
        L->>R: GET token:{token}(验证)
        R-->>L: accountId
        
        L->>R: HGET game_load:*(查询space负载)
        R-->>L: space列表
        L->>L: 选择最优space
        
        L->>S: 转发EnterSpace(accountId, playerData)
        S->>S: 创建space内玩家对象
        S->>R: HSET space:{spaceId}:players {accountId} ...
        
        S-->>L: EnterSpaceAck
        L-->>C: EnterGameRes(spaceAddr)
        
        %% 后续通信
        C->>S: 直接连接space(携带token)
        S->>R: 验证token
        S->>S: 处理游戏逻辑
        
        Note over C,S: 定时心跳保持连接
        C->>S: Heartbeat(每5秒)
        S->>R: HSET player:{accountId} last_heartbeat {timestamp}
    end

十、本章总结与工程建议

mindmap
  root((第10章:分布式登录与Redis))
    分布式架构
      game/space分离
      负载均衡:最小连接数/自定义分数
      Redis作为共享状态中心
    Token机制
      无状态=可扩展
      SETEX + account_token映射
      单点登录:踢旧留新
      续期策略:避免频繁写入
    组件化设计
      继承 -> 组合
      Player = ID + 组件容器
      按需分配内存
    Redis实战
      hiredis连接池
      数据结构选型
      Pipeline批量操作
      内存淘汰策略
    性能优化
      异步日志
      内存池预分配
      ConnectObj轻量化
      延迟加载玩家数据
    数据治理
      登录时检查账号状态
      夜间定时清理
      注销时级联删除
      Pipeline批量操作

老工程师的最后几句

  1. Redis不是银弹:它是内存数据库,内存是贵的。不要把整个数据库塞进Redis,只放热数据。
  2. Pipeline是神器:单个Redis命令的RTT约1ms,100个命令串行执行就是100ms。用Pipeline批量发送,一次RTT完成全部。
  3. 连接池大小有讲究:太多连接浪费资源,太少会阻塞。经验公式:连接数 = CPU核心数 * 2 + 有效磁盘数(来自Redis作者antirez的建议)。
  4. Monitor命令是双刃剑redis-cli monitor可以实时查看所有命令,但生产环境慎用——它会吃掉30%以上的性能。
  5. 持久化策略看场景:纯缓存(断电可重建)= 关持久化;会话存储 = RDB快照;支付相关 = AOF everysec。
  6. 分布式锁要小心:Redis的SET NX EX实现的锁,如果持有锁的进程崩溃且还没执行完,锁会在超时后释放。确保你的超时时间大于业务处理时间。

本章核心代码速查

  • GameProcessLoad::GetLoadScore() — 综合负载计算
  • TokenManager — token生成/验证/销毁
  • Player组件容器 — 类型安全的组件系统
  • RedisConnector — 连接池封装
  • DataCleanupManager — 三级数据清理策略
  • OptimizedMessageComponent — 内存池优化
  • LightConnectObj — 延迟加载设计

"Redis之于游戏服务器,就像脊髓反射之于人体——你可以不用大脑思考就完成基本动作。设计好的Redis层,能让你的分布式架构像呼吸一样自然。" — 凌晨4点,某游戏公司架构师在白板前写下的这句话。