分布式锁的三种主流实现方案,90%的面试官都会追问它们的优劣与选型陷阱!

博主介绍:程序喵大人

在分布式系统架构中,当多个服务实例需要访问同一共享资源时,分布式锁成为了保证数据一致性的关键机制。从秒杀系统防止库存超卖,到定时任务避免重复执行,再到支付回调防止重复处理,分布式锁的身影无处不在。

对于准备后端面试的开发者来说,这几乎是绕不开的话题,而面试官往往会从多个角度进行追问:为什么需要分布式锁?有哪些实现方案?各自的优缺点是什么?如何根据业务场景进行选型?这些问题看似简单,但要让面试官满意,需要对各种方案有深入的理解,能够清晰地阐述其背后的原理和权衡。

在单机多线程环境下,我们可以使用 C++ 的 std::mutexstd::unique_lock 来保证线程安全,但在分布式系统中,多个服务实例运行在不同的机器上,本地锁已经失效了。

秒杀系统就是一个典型的场景。假设某商品库存仅剩 100 件,但在瞬间有数万请求涌来。如果每个服务实例都独立处理库存扣减,那么多个请求可能同时读到库存为 100,都计算出 99,然后先后写入数据库,最终库存变为 99 而不是 98,这意味着卖出了 101 件商品,造成严重的资损。

分布式锁可以确保在扣减库存的整个逻辑段内,只有一个微服务实例能够执行操作,从而保证数据的一致性。同样的问题也出现在定时任务中,多个节点同时触发可能导致同一个数据处理两次,或者在支付回调中重复处理同一笔支付,这些都需要分布式锁来保护。

业界主流的分布式锁实现方案主要有三种:

  • 基于 Redis
  • 基于 ZooKeeper
  • 基于数据库

这三种方案各有特点,适合不同的业务场景,理解它们的实现原理和优缺点,是做出正确技术选型的基础。

一、基于 Redis 的分布式锁

Redis 分布式锁是目前互联网企业最常用的方案,核心优势是高性能。其实现原理利用了 Redis 的 SET 命令的原子性特性。

使用 hiredis 库,我们可以这样实现一个基本的 Redis 分布式锁:

cpp 复制代码
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:只有当 key 不存在时才设置,保证互斥性
    • PX:设置毫秒级过期时间,防止死锁
  • value 使用 UUID

    用于唯一标识锁的持有者,确保只有加锁的客户端才能解锁,避免误删其他客户端的锁。

  • Lua 脚本解锁

    将"校验 value"和"删除 key"两个操作合并为一个原子操作,避免并发问题。

Redis 分布式锁的优缺点

优点

  • 纯内存操作,性能极高,QPS 可达 10 万以上
  • 实现简单,生态成熟

缺点

  • 主从切换可能导致锁丢失
  • 过期时间难以准确评估

Watch Dog 自动续期机制

为了解决锁过期时间难以评估的问题,可以引入 Watch Dog 机制:

  • 获取锁后启动后台守护线程
  • 定期检查锁是否仍然由当前客户端持有
  • 若持有则自动延长过期时间

需要注意:

  • 实现复杂度上升
  • 客户端进程崩溃时,守护线程停止,锁最终仍依赖过期释放

二、基于 ZooKeeper 的分布式锁

ZooKeeper 分布式锁的核心原理与 Redis 完全不同,它利用了 ZooKeeper 的临时顺序节点Watcher 机制

实现原理

  1. 客户端在指定路径下创建一个临时顺序节点,如:
    /locks/lock-000000001
  2. ZooKeeper 保证节点序号全局唯一、单调递增
  3. 客户端获取所有子节点并排序
  4. 若自己节点序号最小,则获得锁
  5. 否则监听前一个序号的节点
  6. 当前一个节点被删除时,收到通知并重新竞争锁

C 客户端示例代码

cpp 复制代码
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_);
    }

    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 分布式锁的优缺点

优点

  • 临时节点随会话消失,天然避免死锁
  • 顺序节点保证公平性,避免饥饿
  • 基于 ZAB 协议,强一致性,不会丢锁

缺点

  • 性能较低,吞吐量一般在 1 万 QPS 左右
  • 部署和运维成本高
  • 需要注意"羊群效应",需通过监听前一个节点来避免

三、基于数据库的分布式锁

基于数据库的分布式锁是三者中最简单的方案,原理直观:

  • 建一张锁表
  • lock_key 建立唯一索引
  • 加锁:插入记录
  • 解锁:删除记录

示例代码

cpp 复制代码
class DatabaseLock {
private:
    sql::mysql::MySQL_Driver* driver_;
    sql::Connection* conn_;
    std::string lockKey_;
    std::string owner_;
    int lockTimeout_;

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());
    }

    bool tryLock() {
        try {
            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, "2025-01-01 00:00:00");
            return pstmt->executeUpdate() > 0;
        } catch (...) {
            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_);
            return pstmt->executeUpdate() > 0;
        } catch (...) {
            return false;
        }
    }
};

数据库分布式锁的优缺点

优点

  • 实现简单
  • 不需要引入新组件
  • 适合小规模系统或原型验证

缺点

  • 性能最差,通常只有几百 QPS
  • 存在死锁风险,需要额外清理机制
  • 长时间占用数据库连接,影响整体性能

四、如何进行分布式锁选型

  • Redis

    • 追求极致性能
    • 高并发场景(秒杀、库存扣减)
    • 可通过幂等性作为兜底
  • ZooKeeper

    • 强一致性优先
    • 金融转账、分布式事务、核心业务
  • 数据库

    • 并发量低
    • 对性能要求不高
    • 临时方案或小型系统

五、面试中的高频追问点

  • Redis 锁为什么要用唯一 value
  • 解锁为什么必须使用 Lua 脚本
  • ZooKeeper 临时节点的生命周期
  • 如何避免 ZooKeeper 羊群效应
  • 数据库锁表的索引设计

分布式锁看似只是一个技术点,背后却涉及并发控制、分布式一致性、性能与可靠性的权衡。真正掌握它,不仅能让你在面试中游刃有余,更能在实际项目中做出正确的技术决策,构建稳定可靠的分布式系统。

码字不易,欢迎大家点赞,关注,评论,谢谢!

相关推荐
ZHOUPUYU8 小时前
PHP 8.3网关优化:我用JIT将QPS提升300%的真实踩坑录
开发语言·php
寻寻觅觅☆12 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
fpcc12 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t12 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
你这个代码我看不懂13 小时前
@RefreshScope刷新Kafka实例
分布式·kafka·linq
赶路人儿13 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12313 小时前
C++使用format
开发语言·c++·算法
码说AI14 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS14 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
lanhuazui1014 小时前
C++ 中什么时候用::(作用域解析运算符)
c++