Chap21-DistributedLock_SingleServer

Chap21-DistributedLock_SingleServer

如标题,本节是只针对但服务器的分布式锁的设计。当一个用户在线时,我们需要考虑当另一个设备的相同用户也要登陆时,如何处理两个设备的登陆以及服务器对该用户的状态变化。

  • 分布式锁

在分布式系统中,多个客户端可能同时访问和操作共享资源。为了防止数据竞争和不一致,分布式锁是一个常见的解决方案。Redis 提供了强大的功能来实现高效且可靠的分布式锁。本文将通过 C++ 和 Redis(通过 hredis 库)实现一个简单的分布式锁。

分布式锁:它是一种机制,用于保证在分布式系统中,某一时刻只有一个客户端能够执行某些共享资源的操作。

使用 Redis 作为锁存储:Redis 被用作集中式存储,可以确保锁的状态在所有参与者之间同步。

image-20251130195139250

  • 思路

    • 加锁:客户端通过设置一个 Redis 键来获取锁。通过 Redis 的原子操作,确保只有一个客户端能够成功设置该键。

    • 持有者标识符:每个客户端在加锁时生成一个唯一的标识符(UUID),该标识符用来标识锁的持有者。

    • 超时机制:锁会在一定时间后自动释放(过期),防止因程序异常导致的死锁。

    • 解锁:只有锁的持有者才能释放锁,这通过 Redis Lua 脚本来保证。

实现步骤

全局唯一标识符

使用 Boost UUID 库生成一个全局唯一的标识符(UUID)。这个标识符会被用作锁的持有者标识符。它确保每个客户端在加锁时拥有唯一的标识,从而能够确保锁的唯一性。

std::string generateUUID() {
    boost::uuids::uuid uuid = boost::uuids::random_generator()();
    return to_string(uuid);
}

AcquireLock尝试加锁

客户端通过 Redis 的 SET 命令尝试加锁。该命令的参数如下:

  • NX:确保只有当键不存在时才能成功设置(即,只有一个客户端能够成功设置锁)。

  • EX:设置一个超时时间,锁会在超时后自动释放,避免死锁。

如果加锁成功,返回一个唯一标识符。如果加锁失败,则会在指定的超时时间内多次尝试。

// 尝试获取锁,返回锁的唯一标识符(UUID),如果获取失败则返回空字符串
std::string acquireLock(redisContext* context, const std::string& lockName, int lockTimeout, int acquireTimeout) {
    std::string identifier = generateUUID();
    std::string lockKey = "lock:" + lockName;
    auto endTime = std::chrono::steady_clock::now() + std::chrono::seconds(acquireTimeout);

    while (std::chrono::steady_clock::now() < endTime) {
        // 使用 SET 命令尝试加锁:SET lockKey identifier NX EX lockTimeout
        redisReply* reply = (redisReply*)redisCommand(context, "SET %s %s NX EX %d", lockKey.c_str(), identifier.c_str(), lockTimeout);
        if (reply != nullptr) {
            // 判断返回结果是否为 OK
            if (reply->type == REDIS_REPLY_STATUS && std::string(reply->str) == "OK") {
                freeReplyObject(reply);
                return identifier;
            }
            freeReplyObject(reply);
        }
        // 暂停 1 毫秒后重试,防止忙等待
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    return "";
}

函数参数说明

redisContext***** context
这是一个指向 Redis 连接上下文的指针,用于与 Redis 服务器通信。通过这个上下文,你可以发送命令和接收响应。

const std::string& lockName
这是你想要加锁的资源名称。例如,如果你需要对某个资源加锁,可以用 "my_resource",函数内部会把它拼接成 "lock:my_resource" 作为 Redis 中的 key。

int lockTimeout
这是锁的有效期,单位是秒。设置这个值的目的是防止因程序异常或崩溃而导致的死锁。当锁达到这个超时时间后,Redis 会自动删除这个 key,从而释放锁。

int acquireTimeout
这是获取锁的最大等待时间,单位也是秒。如果在这个时间内没有成功获取到锁,函数就会停止尝试,并返回空字符串。这样可以避免程序无限等待。

Redis 命令解释

acquireLock 函数中,使用的 Redis 命令格式是:

"SET %s %s NX EX %d"

这个命令实际上是一个格式化字符串,参数会被填入以下位置:

  1. SET
    Redis 的基本命令,用于设置一个 key 的值。

  2. %s(第一个 %s)
    代表锁的 key(例如 "lock:my_resource")。

  3. %s(第二个 %s)
    代表锁的持有者标识符,也就是通过 generateUUID() 生成的 UUID。

  4. NX
    表示 “Not eXists”,意思是“只有当 key 不存在时才进行设置”。这可以保证如果其他客户端已经设置了这个 key(即已经有锁了),那么当前客户端就不会覆盖原来的锁。

  5. EX %d
    EX 参数用于指定 key 的过期时间,%d 表示锁的有效期(lockTimeout),单位为秒。这样即使客户端因某些原因没有正常释放锁,锁也会在指定时间后自动失效。

ReleaseLock释放锁

释放锁的操作使用 Redis Lua 脚本,确保只有持有锁的客户端才能释放锁。脚本通过判断当前锁的持有者是否与传入的标识符一致来决定是否删除锁。

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
  • KEYS[1]:锁的 key(例如 lock:my_resource)。

  • ARGV[1]:客户端在加锁时生成的唯一标识符。

  • 如果当前锁的值(标识符)与传入的标识符一致,删除该锁。

Lua 脚本的作用是:

  • redis.call('get', KEYS[1]):从 Redis 获取 lockKey` 对应的值。

  • if redis.call('get', KEYS[1]) == ARGV[1]:检查获取到的值是否与传入的 identifier 相同,只有标识符匹配时才能删除锁。

  • return redis.call('del', KEYS[1]):如果匹配,执行删除操作,释放锁。

  • else return 0:如果标识符不匹配,返回 0,表示没有成功释放锁。

// 释放锁,只有锁的持有者才能释放,返回是否成功
bool releaseLock(redisContext* context, const std::string& lockName, const std::string& identifier) {
    std::string lockKey = "lock:" + lockName;
    // Lua 脚本:判断锁标识是否匹配,匹配则删除锁
    const char* luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then \
                                return redis.call('del', KEYS[1]) \
                             else \
                                return 0 \
                             end";
    // 调用 EVAL 命令执行 Lua 脚本,第一个参数为脚本,后面依次为 key 的数量、key 以及对应的参数
    redisReply* reply = (redisReply*)redisCommand(context, "EVAL %s 1 %s %s", luaScript, lockKey.c_str(), identifier.c_str());
    bool success = false;
    if (reply != nullptr) {
        // 当返回整数值为 1 时,表示成功删除了锁
        if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1) {
            success = true;
        }
        freeReplyObject(reply);
    }
    return success;
}

封装成类DistributedLock

//.h
#ifndef DISTRIBUTEDLOCK_H
#define DISTRIBUTEDLOCK_H

#include <string>
#include <thread>

struct redisContext;
class DistributedLock {
private:
    DistributedLock() = default;

public:
    ~DistributedLock() = default;
    /**
     * @brief  得到实例
     *
     * @return DistributedLock&
     */
    static DistributedLock& GetInstance();
    /**
     * @brief 在redis上获取分布式锁
     *
     * @param context
     * @param keyname
     * @param timeout
     * @param acquireTimeout
     * @return std::string
     */
    std::string AcquireLock(redisContext* context, const std::string& keyname, int timeout,
        int acquireTimeout);
    /**
     * @brief 释放分布式锁
     *
     * @param key
     * @param identifier
     */
    bool ReleaseLock(redisContext* context, const std::string& keyname, const std::string& identifier);
    std::string GenerateUUID();
};

#endif

//.cpp
#include "DistributedLock.h"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <hiredis/hiredis.h>

using namespace std::chrono_literals;

DistributedLock& DistributedLock::GetInstance()
{
    static DistributedLock instance;
    return instance;
}

std::string DistributedLock::AcquireLock(redisContext* context, const std::string& keyname, int timeout, int acquireTimeout)
{
    std::string idenetifier = GenerateUUID();
    std::string key = "lock_" + keyname;
    auto start = std::chrono::steady_clock::now();
    auto end = start + std::chrono::seconds(acquireTimeout);
    while (std::chrono::steady_clock::now() < end) {
        redisReply* reply = (redisReply*)redisCommand(context, "SET %s %s NX EX %d", key.c_str(), idenetifier.c_str(), timeout);
        if (reply != nullptr) {
            if (reply->type == REDIS_REPLY_STATUS && std::string(reply->str) == "OK") {
                freeReplyObject(reply);
                return idenetifier;
            }
            freeReplyObject(reply);
        }
        std::this_thread::sleep_for(5ms);
    }
    return "";
}

/**
 * 释放分布式锁使用了lua脚本:
 *
 * if redis.call("get",KEYS[1]) == ARGV[1] then
 *   return redis.call("del",KEYS[1])
 * else
 *   return 0
 * end
 *
 */
bool DistributedLock::ReleaseLock(redisContext* context, const std::string& keyname, const std::string& identifier)
{
    std::string key = "lock_" + keyname;
    const char* luaScript = R"(
        if redis.call("get",KEYS[1]) == ARGV[1] then
            return redis.call("del",KEYS[1])
        else
            return 0
        end
    )";

    redisReply* reply = (redisReply*)redisCommand(context, "EVAL %s 1 %s %s", luaScript, key.c_str(), identifier.c_str());
    bool success = false;
    if (reply != nullptr) {
        if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1) {
            success = true;
        }
        freeReplyObject(reply);
    }
    return success;
}

std::string DistributedLock::GenerateUUID()
{
    boost::uuids::uuid uuid = boost::uuids::random_generator()();
    return to_string(uuid);
}

后端使用

首先,我们在LogicSystem加入函数

// 设置Server
void SetServer(std::shared_ptr<Server> server) noexcept;

因为我们需要主动通知旧客户端离线,之后在_server中删除对应的session.

同时,main也要调用设置server

auto server_ptr = std::make_shared<Server>(ioc, std::stoi(port));
server_ptr->Start();
LogicSystem::GetInstance()->SetServer(server_ptr);

在RedisManager中提供对外使用分布式锁的接口

std::string AcquireLock(const std::string& key, int timeout = 5,
        int acquireTimeout = 5);
bool ReleaseLock(const std::string& key, const std::string& identifier);
std::string RedisManager::AcquireLock(const std::string& key, int timeout, int acquireTimeout)
{
    auto connection = _pool->GetConnection();
    if (connection == nullptr) {
        return "";
    }
    Defer defer([&connection, this] {
        _pool->ReturnConnection(connection);
    });

    return DistributedLock::GetInstance().AcquireLock(connection, key, timeout, acquireTimeout);
}

bool RedisManager::ReleaseLock(const std::string& key, const std::string& identifier)
{
    if (identifier.empty()) {
        return false;
    }
    auto connection = _pool->GetConnection();
    if (connection == nullptr) {
        return false;
    }
    Defer defer([&connection, this] {
        _pool->ReturnConnection(connection);
    });
    return DistributedLock::GetInstance().ReleaseLock(connection, key, identifier);
}

之后在LogicSystem的回调函数中处理用户登陆的时候,需要先查询用户是否已经登陆,是则先通知退出,否则直接登陆

auto lock_key = LOCK_PREFIX + uid_str;
auto identifier = RedisManager::GetInstance()->AcquireLock(lock_key, LOCK_TIMEOUT, ACQUIRE_LOCK_TIMEOUT);
Defer defer2([this,identifier,lock_key]{
    RedisManager::GetInstance()->ReleaseLock(lock_key, identifier);
});

std::string uid_ip_value = "";
std::string uid_ip_key = USERIP_PREFIX + uid_str;
bool b_ip = RedisManager::GetInstance()->GetInstance()->Get(uid_ip_key, uid_ip_value);
if (b_ip){
    // 查询到了ip地址,说明用户已经在线了。
    auto&cfg = ConfigManager::GetInstance();
    auto self_name = cfg["SelfServer"]["name"];
    if (uid_ip_value == self_name) {
        // 如果是当前服务器,直接踢掉
        auto old_session = UserManager::GetInstance()->GetSession(uid);
        if (old_session){
            old_session->NotifyOffline(uid);
            _server->ClearSession(uid_str);
        }
    }
    else{
        // 不在本服务器,grpc通知剔除
    }
}

为了使得分布式锁的精度更高一点,我们调整了查询的位置

_function_callbacks[MsgId::ID_CHAT_LOGIN] = [this](std::shared_ptr<Session> session, uint16_t msg_id, const std::string& msg) {
        json j = json::parse(msg);
        auto uid = j["uid"].get<int>();
        auto token = j["token"].get<std::string>();
        SPDLOG_INFO("Thread: {},User {} Login with token {}", thread_id_to_string(std::this_thread::get_id()), uid, token);

        json jj;
        Defer defer([this, &jj, session]() {
            std::string return_str = jj.dump();
            session->Send(return_str, static_cast<int>(MsgId::ID_CHAT_LOGIN_RSP));
        });

        std::string uid_str = std::to_string(uid);
        std::string token_key = USER_TOKEN_PREFIX + uid_str;
        std::string token_value = "";

        bool success = RedisManager::GetInstance()->Get(token_key, token_value);
        if (!success) {
            jj["error"] = ErrorCodes::ERROR_UID_INVALID;
            return;
        }
        if (token_value != token) {
            jj["error"] = ErrorCodes::ERROR_TOKEN_INVALID;
            return;
        }

        std::string base_key = USER_BASE_INFO_PREFIX + uid_str;
        auto user_info = std::make_shared<UserInfo>();
        bool b_base = GetBaseInfo(base_key, uid, user_info);
        if (!b_base) {
            jj["error"] = ErrorCodes::ERROR_UID_INVALID;
            return;
        }

        jj["error"] = ErrorCodes::SUCCESS;


        jj["uid"] = uid;
        jj["name"] = user_info->name;
        jj["email"] = user_info->email;
        jj["nick"] = user_info->nick;
        jj["sex"] = user_info->sex;
        jj["desc"] = user_info->desc;
        jj["icon"] = user_info->icon;
        jj["token"] = token;

        // 获取申请列表
        std::vector<std::shared_ptr<UserInfo>> apply_list;
        bool b_apply = MysqlManager::GetInstance()->GetFriendApplyList(uid_str, apply_list);
        if (b_apply && apply_list.size() > 0) {
            // 我们这里规定哪怕数据库操作成功,但是没有数据也算失败,就直接跳过,避免多余判断。
            json apply_friends;
            for (auto& apply_user : apply_list) {
                json apply_friend;
                apply_friend["uid"] = apply_user->uid;
                apply_friend["name"] = apply_user->name;
                apply_friend["email"] = apply_user->email;
                apply_friend["icon"] = apply_user->icon;
                apply_friend["sex"] = apply_user->sex;
                apply_friend["desc"] = apply_user->desc;
                apply_friend["back"] = apply_user->back; // 时间
                apply_friends.push_back(apply_friend);
            }
            jj["apply_friends"] = apply_friends;
        }
        // 获取通知列表
        std::vector<std::shared_ptr<UserInfo>> notification_list;
        bool b_notify = MysqlManager::GetInstance()->GetNotificationList(uid_str, notification_list);
        if (b_notify && notification_list.size() > 0) {
            json notifications;
            for (auto& notification : notification_list) {
                json item;
                item["uid"] = notification->uid;
                item["type"] = notification->status; // 用status代表type借用UserInfo的结构。
                item["message"] = notification->desc; // 用desc代表message借用UserInfo的结构。
                item["time"] = notification->back; // 备用字段表示时间。
                notifications.push_back(item);
            }
            jj["notifications"] = notifications;
        }

        // 获取会话列表
        std::vector<std::shared_ptr<SessionInfo>> session_list;
        bool b_session = MysqlManager::GetInstance()->GetSeessionList(uid_str, session_list);
        if (b_session && session_list.size() > 0) {
            json conversations;
            for (auto& session_item : session_list) {
                json conversation;
                conversation["uid"] = session_item->uid;
                conversation["from_uid"] = session_item->from_uid;
                conversation["to_uid"] = session_item->to_uid;
                conversation["create_time"] = session_item->create_time;
                conversation["update_time"] = session_item->update_time;
                conversation["name"] = session_item->name;
                conversation["icon"] = session_item->icon;
                conversation["status"] = session_item->status;
                conversation["deleted"] = session_item->deleted;
                conversation["pined"] = session_item->pined;
                conversations.push_back(conversation);
            }
            jj["conversations"] = conversations;
        }

        // 获取好友列表
        std::vector<std::shared_ptr<UserInfo>> friend_list;
        std::vector<int> online_friends;

        bool b_friend = MysqlManager::GetInstance()->GetFriendList(uid_str, friend_list);
        online_friends.resize(friend_list.size());
        if (b_friend && friend_list.size() > 0) {
            json friends;
            for (std::size_t i = 0; i < friend_list.size(); i++) {
                auto& friend_user = friend_list[i];
                json friend_item;
                // 查询状态
                std::string status_key = USER_STATUS_PREFIX + std::to_string(friend_user->uid);
                std::string status_value;
                bool b_status = RedisManager::GetInstance()->Get(status_key, status_value);
                if (b_status) {
                    friend_item["status"] = std::stoi(status_value);
                    online_friends[i] = friend_item["status"];
                } else {
                    friend_item["status"] = 0;
                    online_friends[i] = 0;
                }
                friend_item["uid"] = friend_user->uid;
                friend_item["name"] = friend_user->name;
                friend_item["email"] = friend_user->email;
                friend_item["icon"] = friend_user->icon;
                friend_item["sex"] = friend_user->sex;
                friend_item["desc"] = friend_user->desc;
                friends.push_back(friend_item);
            }
            jj["friends"] = friends;
        }

        // 获取未读消息
        std::vector<std::shared_ptr<im::MessageItem>> unread_messages;
        bool b_unread = MysqlManager::GetInstance()->GetUnreadMessages(uid_str, unread_messages);
        if (b_unread && unread_messages.size() > 0) {
            json messages = json::array();
            for (auto& message : unread_messages) {
                json message_item;
                message_item["id"] = message->id();
                message_item["from_id"] = message->from_id();
                message_item["to_id"] = message->to_id();
                message_item["timestamp"] = message->timestamp();
                message_item["env"] = message->env();
                message_item["content_type"] = message->content().type();
                message_item["content_data"] = message->content().data();
                SPDLOG_INFO("data:{}", message->content().data());
                message_item["content_mime_type"] = message->content().mime_type();
                message_item["content_fid"] = message->content().fid();
                messages.push_back(message_item);
            }
            jj["unread_messages"] = messages;
        }

        auto lock_key = LOCK_PREFIX + uid_str;
        auto identifier = RedisManager::GetInstance()->AcquireLock(lock_key, LOCK_TIMEOUT, ACQUIRE_LOCK_TIMEOUT);
        Defer defer2([this,identifier,lock_key]{
            RedisManager::GetInstance()->ReleaseLock(lock_key, identifier);
        });

        std::string uid_ip_value = "";
        std::string uid_ip_key = USERIP_PREFIX + uid_str;
        bool b_ip = RedisManager::GetInstance()->GetInstance()->Get(uid_ip_key, uid_ip_value);
        if (b_ip){
            // 查询到了ip地址,说明用户已经在线了。
            auto&cfg = ConfigManager::GetInstance();
            auto self_name = cfg["SelfServer"]["name"];
            if (uid_ip_value == self_name) {
                // 如果是当前服务器,直接踢掉
                auto old_session = UserManager::GetInstance()->GetSession(uid);
                if (old_session){
                    old_session->NotifyOffline(uid);
                    _server->ClearSession(uid_str);
                }
            }
            else{
                // 不在本服务器,grpc通知剔除
            }
        }

        // 将登陆人的状态信息改变为1
        std::string key = USER_STATUS_PREFIX + uid_str;
        RedisManager::GetInstance()->Set(key, "1");

        // 登陆成功,通知所有在线好友
        // 上面得到了好友列表,这里通知所有在线好友
        for (std::size_t i = 0; i < friend_list.size(); i++) {
            auto& friend_uid = friend_list[i]->uid;
            std::string ip_key = USERIP_PREFIX + std::to_string(friend_uid);
            std::string ip_value;
            bool b_ip = RedisManager::GetInstance()->Get(ip_key, ip_value);
            if (b_ip) {
                if (online_friends[i] == 1) {
                    auto& cfg = ConfigManager::GetInstance();
                    auto self_name = cfg["SelfServer"]["name"];
                    if (ip_value == self_name) {
                        auto session2 = UserManager::GetInstance()->GetSession(friend_uid);
                        if (session2) {
                            SPDLOG_INFO("FROM UID:{},to:{}", uid, friend_uid);
                            json j;
                            j["error"] = ErrorCodes::SUCCESS;
                            j["uid"] = uid;
                            j["message"] = "😁好友" + user_info->name + "上线了😄";
                            j["type"] = static_cast<int>(NotificationCodes::ID_NOTIFY_FRIEND_ONLINE);
                            j["status"] = 1;
                            // 当前时间
                            auto now = std::chrono::system_clock::now();
                            auto time_t = std::chrono::system_clock::to_time_t(now);

                            std::stringstream ss;
                            ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
                            j["time"] = ss.str();
                            j["icon"] = user_info->icon;
                            session2->Send(j.dump(), static_cast<int>(MsgId::ID_NOTIFY));
                        }
                    } else {
                        NotifyFriendOnlineRequest request;
                        request.set_fromuid(uid);
                        request.set_touid(friend_uid);
                        request.set_name(user_info->name);
                        request.set_type(static_cast<int>(NotificationCodes::ID_NOTIFY_FRIEND_ONLINE));
                        request.set_message("😁好友" + user_info->name + "上线了😄");
                        request.set_icon(user_info->icon);
                        // 当前时间
                        auto now = std::chrono::system_clock::now();
                        auto time_t = std::chrono::system_clock::to_time_t(now);
                        std::stringstream ss;
                        ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S");
                        request.set_time(ss.str());

                        ChatGrpcClient::GetInstance()->NotifyFriendOnline(ip_value, request);
                    }
                }
            }
        }

        // 更新登陆数量
        auto server_name = ConfigManager::GetInstance()["SelfServer"]["name"];
        auto count_str = RedisManager::GetInstance()->HGet(LOGIN_COUNT_PREFIX, server_name);
        int count = 0;
        if (!count_str.empty()) {
            count = std::stoi(count_str);
        }
        count++;
        count_str = std::to_string(count);
        RedisManager::GetInstance()->HSet(LOGIN_COUNT_PREFIX, server_name, count_str);

        // session绑定uid
        session->SetUid(uid);
        // 绑定连接的服务器名称和用户uid
        std::string ip_key = USERIP_PREFIX + std::to_string(uid);
        RedisManager::GetInstance()->Set(ip_key, server_name);
        // uid和session绑定管理,方便之后踢人
        UserManager::GetInstance()->SetUserSession(uid, session);
        // 设置用户状态在线
        std::string status_key = USER_STATUS_PREFIX + uid_str;
        RedisManager::GetInstance()->Set(status_key, "1");
    };

一部分查询放置在了锁之外,只在涉及到共享的状态信息的地方使用分布式锁。

踢人的时候,调用了

_server->ClearSession(uid_str);

但是不只有这时候会调用ClearSession,遇到用户因为网络问题,或者主动退出也要调用

例如,我们的服务器对应的session一直在异步读/写,当遇到问题的时候,就会Close,

void Session::HandleWrite(boost::system::error_code ec, std::shared_ptr<Session> self)
{
    try {
        if (ec) {
            SPDLOG_WARN("Handle Write Filed,Errir is {}", ec.what());
            Close();
        }
        std::lock_guard<std::mutex> lock(_send_lock);
        _send_queue.pop();
        if (!_send_queue.empty()) {
            auto msgnode = _send_queue.front();
            boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),
                std::bind(&Session::HandleWrite, this, std::placeholders::_1, shared_from_this()));
        }

    } catch (std::exception& e) {
        SPDLOG_WARN("Exception code:{}", e.what());
        Close();
    }
}

当出现问题的时候,Close,在这之中,我们也使用了分布式锁,防止共享信息的不一致问题

void Session::Close()
{
    // 我们把分布式的锁的操作等都放在了这一个close中,防止多处代码重复

    if (_stop) {
        return;
    }
    _stop = true;
    {
        std::lock_guard<std::mutex> lock(_send_lock);
        while (!_send_queue.empty()) {
            _send_queue.pop();
        }
    }

    // 安全关闭socket
    boost::system::error_code ec = _socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    if (ec) {
        SPDLOG_WARN("Socket shutdown error: {}", ec.message());
    }

    ec = _socket.close(ec);
    if (ec) {
        SPDLOG_WARN("Socket close error: {}", ec.message());
    }
    SPDLOG_INFO("Session {} disconnected!", _session_id);

    auto uid_str = std::to_string(_uid);
    auto lock_key = LOCK_PREFIX + uid_str;
    auto identifier = RedisManager::GetInstance()->AcquireLock(lock_key, LOCK_TIMEOUT, ACQUIRE_LOCK_TIMEOUT);

    Defer defer([identifier, lock_key, this]() {
        _server->ClearSession(_session_id);
        RedisManager::GetInstance()->ReleaseLock(lock_key, identifier);
    });

    if (identifier.empty()) {
        return;
    }

    std::string redis_session_id = "";
    bool b_session = RedisManager::GetInstance()->Get(USER_SESSION_PREFIX + uid_str, redis_session_id);
    if (!b_session) {
        // 没有查询到,说明没有登陆信息
        return;
    }
    if (redis_session_id != _session_id) {
        // 说明有客户在其他的服务器异地登录了
        // 这时候不需要修改/删除信息,这时候已经是新登陆的信息了
        return;
    }

    // 减少登录计数
    RedisManager::GetInstance()->Decr(LOGIN_COUNT_PREFIX + _server->GetServerName());
    // 删除用户服务器信息
    RedisManager::GetInstance()->Del(USERIP_PREFIX + std::to_string(_uid));
    // 删除用户token
    RedisManager::GetInstance()->Del(USER_TOKEN_PREFIX + std::to_string(_uid));
    // 修改用户的状态
    RedisManager::GetInstance()->Set(USER_STATUS_PREFIX + std::to_string(_uid), std::to_string(0));
}

前端

当后端发送ID_NOTIFY_OFF_LINE_REQ的时候,我们前端就要主动退出,回到登陆界面

/**
 * @brief 通知下线了
 */
_handlers[RequestType::ID_NOTIFY_OFF_LINE_REQ] = [this](RequestType requestType,int len,QByteArray data){
    QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
    if (jsonDoc.isNull()){
        return;
    }
    QJsonObject jsonObj = jsonDoc.object();
    if (!jsonObj.contains("error") || jsonObj["error"].toInt()!=static_cast<int>(ErrorCodes::SUCCESS)){
        return;
    }

    if (jsonObj["uid"].toInt() == UserManager::GetInstance()->GetUid()){
        _socket.disconnectFromHost();

        // 弹出对话框提示
        QMessageBox msgBox;
        msgBox.setWindowTitle("连接提示");
        msgBox.setText("您的账号在另一台设备登录,当前连接将被断开。");
        msgBox.setIcon(QMessageBox::Information);
        msgBox.setStandardButtons(QMessageBox::Ok);

        // 显示对话框并等待用户确认
        msgBox.exec();

        emit on_switch_login();
    }
};

我们在mainwindow中接受信号

// 登陆界面跳转登陆界面
 connect(TcpManager::GetInstance().get(),&TcpManager::on_switch_login,this,[this](){
    setupUI();
    stack = new AuthStack(this);
    stack->show();
    setCentralWidget(stack);
    show();
    raise();
    activateWindow();
});

注意这里为什么又要重新的new一遍?那是因为之前设置了父亲,

stack = new AuthStack(this);
setCentralWidget(stack);

当mainwindow使用setCentralWidget替换了中心部件,这时候原来的stack就会析构,所以需要重新new.

总结

image-20251130200626393

这一节只是实现了单服务器的踢人,下一节实现分布式的踢人。

posted @ 2025-12-24 23:18  大胖熊哈  阅读(1)  评论(0)    收藏  举报