找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1395

积分

0

好友

179

主题
发表于 2026-2-11 15:16:41 | 查看: 33| 回复: 0

在分布式系统架构中,当多个服务实例需要访问同一共享资源时,分布式锁成为了保证数据一致性的关键机制。从秒杀系统防止库存超卖,到定时任务避免重复执行,再到支付回调防止重复处理,分布式锁的身影无处不在。对于准备后端开发面试或正在设计高并发系统的同学来说,深刻理解其实现原理与选型陷阱至关重要。

在单机多线程环境下,我们可以使用 C++ 的 std::mutexstd::unique_lock 来保证线程安全,但在分布式系统中,多个服务实例运行在不同的机器上,本地锁已经失效了。秒杀系统就是一个典型的场景,假设某商品库存仅剩 100 件,但在瞬间有数万请求涌来。如果每个服务实例都独立处理库存扣减,那么多个请求可能同时读到库存为 100,都计算出 99,然后先后写入数据库,最终库存变为 99 而不是 98,这意味着卖出了 101 件商品,造成严重的资损。分布式锁可以确保在扣减库存的整个逻辑段内,只有一个微服务实例能够执行操作,从而保证数据的一致性。同样的问题也出现在定时任务中,多个节点同时触发可能导致同一个数据处理两次,或者在支付回调中重复处理同一笔支付,这些都需要分布式锁来保护。

分布式锁主流方案对比图

业界主流的分布式锁实现方案主要有三种:基于 Redis、基于 ZooKeeper 和基于数据库。这三种方案各有特点,适合不同的业务场景,理解它们的实现原理和优缺点,是做出正确技术选型的基础。

基于 Redis 的分布式锁:高性能首选

Redis 分布式锁是目前互联网企业最常用的方案,核心优势是高性能。其实现原理利用了 Redis 的 SET 命令的原子性特性。使用 hiredis 库,我们可以这样实现一个基本的 Redis 分布式锁:

class RedisLock {
private:
    redisContext* redis_;
    std::string lockKey_;
    std::string uniqueValue_;
    int lockTimeout_;

    std::string generateUniqueId() {
        uuid_t uuid;
        uuid_generate(uuid);
        char uuid_str[37];
        uuid_unparse(uuid, uuid_str);
        return std::string(uuid_str);
    }

public:
    RedisLock(const std::string& host, int port,
              const std::string& lockKey, int timeout)
        : lockKey_(lockKey), lockTimeout_(timeout) {
        redis_ = redisConnect(host.c_str(), port);
        if (redis_ == nullptr || redis_->err) {
            throw std::runtime_error("Failed to connect to Redis");
        }
        uniqueValue_ = generateUniqueId();
    }

    bool tryLock() {
        std::string command = "SET " + lockKey_ + " " + uniqueValue_ +
                             " NX PX " + std::to_string(lockTimeout_ * 1000);

        redisReply* reply = (redisReply*)redisCommand(redis_, command.c_str());

        bool success = false;
        if (reply) {
            success = (reply->type == REDIS_REPLY_STATUS &&
                       std::string(reply->str) == "OK");
            freeReplyObject(reply);
        }

        return success;
    }

    bool unlock() {
        std::string 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(
            redis_, "EVAL %s 1 %s %s",
            luaScript.c_str(),
            lockKey_.c_str(),
            uniqueValue_.c_str()
        );

        bool success = false;
        if (reply) {
            success = (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1);
            freeReplyObject(reply);
        }

        return success;
    }
};

这里有几个关键点需要特别注意。加锁使用 SET key value NX PX timeout 命令,NX 参数确保只有在键不存在时才会设置成功,这保证了互斥性;PX 参数设置毫秒级的过期时间,可以防止死锁。value 使用 UUID 作为唯一标识,这一点非常重要,它确保了只有锁的持有者才能释放锁,避免了误删其他客户端的锁。解锁时使用 Lua 脚本,将“检查 value”和“删除键”两个操作合并为一个原子操作,同样是为了保证安全性。

Redis分布式锁实现原理图

Redis 分布式锁的性能极高,纯内存操作,QPS 可达 10 万以上,实现也比较简单,hiredis 库提供了良好的 C++ 封装。但 Redis 锁也存在一些问题。最常见的是主从切换导致的锁丢失。在 Redis 主从架构下,如果主节点在将锁信息同步到从节点之前就宕机了,从节点升级为主节点后,新的客户端就可以在同一资源上获取锁,而原客户端可能还在执行业务逻辑,这样就出现了两个客户端同时持有锁的情况。另一个问题是过期时间难以把握,如果设置得太短,业务逻辑还没执行完锁就过期了;如果设置得太长,服务宕机后锁需要很久才能自动释放。

针对过期时间的问题,可以引入 Watch Dog 机制自动续期。其原理是在获取锁后启动一个后台守护线程,定期检查锁是否仍然持有,如果持有就自动延长过期时间。这样即使业务逻辑执行时间超过了初始设定的过期时间,锁也不会提前释放。但要注意,Watch Dog 机制会增加实现的复杂度,而且如果客户端进程崩溃,守护线程也就停止了,锁最终会因为过期而自动释放,这又回到了过期时间的问题。

基于 ZooKeeper 的分布式锁:强一致性的保障

ZooKeeper 分布式锁的核心原理与 Redis 完全不同,它利用了 ZooKeeper 的临时顺序节点和 Watcher 机制。当一个客户端想要获取锁时,会在指定的路径下创建一个临时顺序节点,比如 /locks/lock-000000001。所有创建的节点都会自动获得一个递增的序号,ZooKeeper 会保证序号的全局唯一性和单调递增。客户端获取所有子节点列表,检查自己创建的节点序号是否是最小的,如果是,就获得锁;如果不是,则监听比自己序号小 1 的那个节点。当锁持有者完成任务后,删除自己的节点,ZooKeeper 会通知监听该节点的下一个客户端,被唤醒的客户端重新检查自己是否成为了最小节点,如果是就获得锁。

ZooKeeper分布式锁原理图

使用 ZooKeeper C 客户端实现的分布式锁大致如下:

class ZkLock {
private:
    zhandle_t* zh_;
    std::string lockPath_;
    std::string currentPath_;
    std::string previousPath_;
    std::string lockName_;

    static void watcher(zhandle_t* zh, int type, int state,
                       const char* path, void* watcherCtx) {
        if (type == ZOO_DELETED_EVENT) {
            ZkLock* lock = static_cast<ZkLock*>(watcherCtx);
            lock->checkLock();
        }
    }

    void checkLock() {
        String_vector children;
        int rc = zoo_get_children(zh_, lockPath_.c_str(), 0, &children);

        if (rc != ZOK) return;

        std::vector<std::string> sortedNodes;
        for (int i = 0; i < children.count; i++) {
            sortedNodes.push_back(children.data[i]);
        }
        std::sort(sortedNodes.begin(), sortedNodes.end());

        std::string currentNode = currentPath_.substr(lockPath_.length() + 1);

        auto it = std::find(sortedNodes.begin(), sortedNodes.end(), currentNode);
        if (it == sortedNodes.begin()) return;

        --it;
        previousPath_ = lockPath_ + "/" + *it;
        zoo_wexists(zh_, previousPath_.c_str(), watcher, this, nullptr);
    }

public:
    ZkLock(const std::string& hosts, const std::string& lockName)
        : lockName_(lockName) {
        zh_ = zookeeper_init( hosts.c_str(), watcher, 10000, 0, this, 0);
        if (!zh_) throw std::runtime_error("Failed to connect to ZooKeeper");

        lockPath_ = "/locks/" + lockName_;
        ensurePathExists(lockPath_);
    }

    ~ZkLock() {
        if (zh_) zookeeper_close(zh_);
    }

    void ensurePathExists(const std::string& path) {
        char path_buffer[1024];
        int rc = zoo_create(zh_, path.c_str(), "", 0,
                           &ZOO_OPEN_ACL_UNSAFE, 0, path_buffer, sizeof(path_buffer));

        if (rc != ZOK && rc != ZNODEEXISTS) {
            throw std::runtime_error("Failed to create path");
        }
    }

    bool lock() {
        char path_buffer[1024];
        int rc = zoo_create(zh_, (lockPath_ + "/lock-").c_str(), "", 0,
                           &ZOO_OPEN_ACL_UNSAFE, ZOO_EPHEMERAL | ZOO_SEQUENCE,
                           path_buffer, sizeof(path_buffer));

        if (rc != ZOK) return false;

        currentPath_ = path_buffer;
        checkLock();
        return true;
    }

    bool unlock() {
        int rc = zoo_delete(zh_, currentPath_.c_str(), -1);
        return rc == ZOK;
    }
};

ZooKeeper 分布式锁的优势在于可靠性。临时节点与客户端会话绑定,当客户端与 ZooKeeper 断开连接或崩溃时,节点会自动删除,锁随之释放,天然避免了死锁。顺序节点保证了锁的公平性,按创建顺序获取锁,避免了饥饿问题。基于 ZAB 协议的强一致性保证了锁状态的正确性,不存在 Redis 主从切换导致锁丢失的问题。

但 ZooKeeper 锁的性能相对较低,每次加锁和解锁都需要与 ZooKeeper 交互,涉及网络通信和磁盘 I/O,吞吐量通常在 1 万 QPS 左右。部署和维护成本也较高,需要搭建和维护 ZooKeeper 集群。还有一个需要注意的问题是“羊群效应”,如果大量客户端监听同一个节点,当该节点被删除时,所有监听者都会收到通知,可能导致通知风暴。解决方法是使用顺序节点,每个客户端只监听前一个节点,这样每次锁释放时只有一个客户端被唤醒。

基于数据库的分布式锁:简单但低效

基于数据库的分布式锁是三者中最简单易实现的方案,原理也很直观:利用数据库的唯一索引或事务特性来实现互斥。创建一张锁表,对 lock_key 字段加上唯一索引,加锁时插入记录,解锁时删除记录。如果插入成功说明获取锁成功,如果因为唯一约束冲突而失败,说明锁已被占用。

class DatabaseLock {
private:
    sql::mysql::MySQL_Driver* driver_;
    sql::Connection* conn_;
    std::string lockKey_;
    std::string owner_;
    int lockTimeout_;

    std::string getCurrentTimestamp() {
        auto now = std::chrono::system_clock::now();
        auto in_time_t = std::chrono::system_clock::to_time_t(now);
        std::stringstream ss;
        ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %H:%M:%S");
        return ss.str();
    }

public:
    DatabaseLock(const std::string& host, const std::string& user,
                 const std::string& password, const std::string& database,
                 const std::string& lockKey, int timeout)
        : lockKey_(lockKey), lockTimeout_(timeout) {
        driver_ = sql::mysql::get_mysql_driver_instance();
        conn_ = driver_->connect(host, user, password);
        conn_->setSchema(database);
        owner_ = "127.0.0.1:" + std::to_string(getpid());
    }

    ~DatabaseLock() {
        if (conn_) delete conn_;
    }

    bool tryLock() {
        try {
            auto now = std::chrono::system_clock::now();
            auto expireTime = now + std::chrono::seconds(lockTimeout_);
            auto expireTime_t = std::chrono::system_clock::to_time_t(expireTime);
            std::stringstream ss;
            ss << std::put_time(std::localtime(&expireTime_t), "%Y-%m-%d %H:%M:%S");

            std::unique_ptr<sql::PreparedStatement> pstmt(
                conn_->prepareStatement(
                    "INSERT INTO distributed_lock (lock_key, owner, expire_time) "
                    "VALUES (?, ?, ?)"
                )
            );

            pstmt->setString(1, lockKey_);
            pstmt->setString(2, owner_);
            pstmt->setString(3, ss.str());

            int rows = pstmt->executeUpdate();
            return rows > 0;

        } catch (sql::SQLException& e) {
            return false;
        }
    }

    bool unlock() {
        try {
            std::unique_ptr<sql::PreparedStatement> pstmt(
                conn_->prepareStatement(
                    "DELETE FROM distributed_lock "
                    "WHERE lock_key = ? AND owner = ?"
                )
            );

            pstmt->setString(1, lockKey_);
            pstmt->setString(2, owner_);

            int rows = pstmt->executeUpdate();
            return rows > 0;

        } catch (sql::SQLException& e) {
            return false;
        }
    }
};

数据库锁的优势在于实现最简单,不需要引入额外的中间件,利用现有数据库即可,开发成本低。但它的性能也是最差的,涉及磁盘 I/O 和数据库事务处理,并发支持有限,通常只能支持几百 QPS。还有死锁风险,如果服务在持有锁期间崩溃而没有主动删除锁记录,锁就会一直存在,需要额外的定时任务来清理过期的锁。同时,长时间持有数据库连接和事务,会对数据库连接池造成压力。

分布式锁方案对比表格

方案对比与选型决策

这三种方案各有适用的场景,如何选择需要根据具体的业务需求来权衡。Redis 凭借极高的性能,成为大多数互联网业务的首选,尤其是在秒杀、库存扣减、缓存更新等高并发场景中。虽然存在主从切换丢锁的风险,但通过业务层的幂等性校验可以作为兜底方案,整体上性能收益巨大。

对于金融转账、分布式事务等对数据一致性要求极高的场景,ZooKeeper 是更合适的选择。它的强一致性和可靠性能够保证不会出现锁丢失的情况,虽然性能不如 Redis,但在这些场景中,正确性远比性能重要。

数据库锁则适合并发量很低、对性能要求不高的场景,或者作为临时方案、原型开发使用。如果业务系统已经部署了数据库,使用数据库锁可以快速实现分布式锁功能,无需引入新的组件,对于小规模的内部系统来说是个不错的选择。

分布式锁选型决策树

总结

分布式锁的选型没有银弹,每种方案都有自己的优缺点和适用场景。理解它们的实现原理、权衡点,并结合实际的业务需求,才能做出正确的技术决策。Redis 追求性能,适合高并发;ZooKeeper 追求一致,适合核心业务;数据库追求简单,适合低频场景。在实际项目中,也可以采用混合策略,比如在秒杀系统中使用 Redis 来控制并发,而在核心的库存扣减和订单生成环节使用 ZooKeeper 来保证强一致性。

对于开发者来说,除了理解各种方案的基本原理,还需要关注一些细节和最佳实践。比如 Redis 锁中 value 为什么要用唯一标识、解锁时为什么要用 Lua 脚本、ZooKeeper 临时节点的生命周期、数据库锁表的索引设计等等。无论你是准备 后端面试 还是正在设计分布式系统,这些知识都至关重要。

分布式锁看似是一个简单的技术点,但背后涉及并发控制、分布式系统原理、性能优化等多个方面的知识。真正掌握它,不仅能够帮助你应对面试,更能在实际工作中做出正确的技术决策,构建出更加稳定可靠的分布式系统。在云栈社区,你还可以找到更多关于系统架构和并发编程的深度讨论与实践分享。




上一篇:2026年值得一试的6款跨平台效率工具推荐
下一篇:字节开源桌面GUI自动化工具UI-TARS-Desktop:基于AI的本地与远程操作指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 18:10 , Processed in 0.342008 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表