Node.js中使用Redis实现分布式锁的方案及性能测试

概述

分布式锁是分布式系统中常用的一种同步机制,用来协调分布式系统各个节点之间对共享资源的访问。Redis作为一款高性能的缓存和键值数据库,其亦广泛用于分布式系统中,特别是用于实现分布式锁机制。

本文将探讨在Node.js中如何使用Redis实现分布式锁,并涉及性能测试。最终目标是让读者能够对Redis分布式锁有更深入的了解,并能够为自己的分布式系统中使用Redis实现锁机制提供参考。

Redis分布式锁方案

Redis SETNX 操作

Redis中的SETNX(SET if Not eXists)操作可用来实现分布式锁。该操作语法如下:

----- --- -----

该命令会将给定的keyvalue设定为Redis中的键值对(key-value pair),仅当该key不存在时(返回值为1),否则相当于不执行任何操作(返回值为0)。例如,在Node.js中使用redis模块实现这一操作:

----- ----- - -----------------
----- ------ - ---------------------

---------------------- --------- ----- ------ -- -
  -- ----- ----- ----
  -- ------ --- -- -
    -- -----
    -- ---
  - ---- -
    -- -----
    -- ---
  -
---

对于该实现方案,需要注意以下几点:

  • 在分布式系统中,多个节点都可能同时尝试获取一个key的锁。因此,在使用SETNX前,需要为key指定一定的规则,以确保多个节点能够同时尝试获取该key的锁,例如在key前面增加一个前缀lock:来区分锁。

  • 考虑到多个节点同时尝试获取锁的情况,需要指定一个锁的超时时间,以避免锁死的情况。例如,在上面的例子中,我们可以添加EX选项来设置锁的超时时间为5秒:

    --------------------------- --------- ----- -- ----- ------ -- -
      -- ---
    ---

    每个获取锁成功的节点都需要在自己超时时间到期之前,使用DEL命令手动释放锁:

    ------------------------- ----- ------ -- -
      -- ---
    ---

Redis BLPOP 命令

虽然SETNX操作实现分布式锁是可行的,但其存在两个主要的问题:一、无法自动释放锁(需要使用DEL手动释放);二、如果获取锁的节点超时或崩溃,锁无法自动续约,存在死锁风险。

Redis提供了一种有效的替代方案:使用BLPOP命令。该命令可用于在Redis中实现队列(queue)的功能,同时也可以用来实现锁的功能。

其原理是每个获取锁的节点都会向Redis中一个key的队列(queue)添加一个值。第一个添加成功的节点会获取该锁,而其他节点会被阻塞在BLPOP命令上,直到获取锁的节点释放锁后,其他节点才能够重新尝试获取锁。

使用BLPOP实现分布式锁的过程比使用SETNX方法要复杂一些。首先需要为key设置一个锁的值,该值必须在后续更新操作中使用。我们可以使用一个UUID来作为该值:

----- ---- - -------------------
----- --------- - -------

根据这个lockValue值,我们可以定义一个获取锁的函数如下:

----- -------- ------------------- --------- ----------------- -------------- -
  ----- ------- - -------------------
  ----- ---- - -------------------
  ----- --------- - -------
  ----- --- - ---------- - -----------------

  ----- ----------- - ---- -
    ----- ------ - ----- ------------------- ---------- ----- -------------- ------
    -- ------- --- ----- -
      -- -----
      ------ - -------- --------- --
    -
    ----- --- ----------------- -- ------------------- -----
  -
  ------ -----
-

该获取锁函数将一直循环,直到过期时间到达或成功获取锁。在每次尝试获取锁时,函数都会使用SET命令将锁的值添加到要保护的key中。该命令使用了所有的选项参数:PX表示过期时间为lockTimeoutMS毫秒,其中NX表示仅当该key不存在时才能够添加成功,否则返回null。如果SET命令成功,则表示该获取锁的节点已经拥有该锁;否则需要重试直至成功或超时。

为避免获取锁节点崩溃或超时,我们需要使用BLPOP命令来自动续约,和释放锁。由于该命令会阻塞Redis的IO操作,因此我们需要将其放置于单独的进程或线程中:

----- -------- ------------------- --------- ---------- -------------- -
  ----- --------- - ---------------------
  ----- ------ -
    ----- --- - ---------- - ------------- - - - --
    ----- --------------
      --------------- ---------- ----- -------------- -----
      ---------------
      --------
    -- ---- - ----------- -
      -- -------- - ------------- - ---- --- ------
      ----- --- --------- -- ------------- ------
    -
  -
-

该函数将循环等待,同时使用SET命令设置一个锁的超时时间,并使用DEL命令手动释放锁。循环每次执行完后等待lockTimeoutMS / 2毫秒,以保证会在锁的过期前自动续约。

最后,综合以上两个函数,我们得到一个完整的分布式锁实现方案:

----- -------- ------------------------------ --------- ---------- -
  ----- - -------- --------- - - ----- ------------------- --------- ---------- -----------
  -- --------- -
    ----- ------- - -------------------
    ----- ------------- - -------------
      -----------
      ---------- --------- ---------- ----------------------
      - ------- ---- --
    --
    ------ - -------- ---------- ------------- --
  -
  ------ -----
-

----- -------- ------------------------------ - -------- ---------- ------------- -- -
  ----- --------- - --------------------
  ----- --------------------
  ----- --- ----------------- -- ------------------------ ----------
  ----- ----------------------
-

----- -------- ------ -
  ----- -------- --------- ---------- ---------- - ----------------------
  -- ------- --- ------- -
    ----- ------ - ---------------------
    ----- ------------------- --------- ---------- ------------------- -----
  - ---- -- ------- --- --------- -
    ----- ------ - ---------------------
    ----- ------------------------------ --------- ------------------- -----
  -
-

-- ------------- --- ------- -
  -------------- -- ------------------
-

在主流程函数main中,根据命令行参数对分布式锁的两种情况进行处理:

  • locker情况使用acquireDistributedLock函数获取锁,并使用startLocker函数启动一个进程来处理自动续约和手动释放锁操作。
  • lock情况使用startLocker函数启动一个进程来后台运行自动续约操作。

至此,我们已经完成了完整的分布式锁实现方案。

性能测试

为了测试我们的分布式锁实现方案的性能和效率,我们需要构建一个短时的压力测试。在压力测试中,我们将同时启动多个(上千个)同时尝试获取锁的节点,以测试分布式锁在高并发场景下的性能表现。

以下是测试代码实现:

----- - --------- - - ----------------
----- ----- - -----------------
----- ---- - -------------------
----- ------ - ---------------------
----- ---------------- - ----------------------------------
----- ---------------- - ----------------------------------
----- - - ----
----- --------- - -----

----- -------- --------- -
  --- ------- - --
  --- ------- - --
  ----- -------- - -------
  ----- ----- - -----------
  ----- ----- - ---------------------- -- ------------------------

  --- -
    ----- -------------------
  - ----- --- -
    -----------------
  - ------- -
    ----- --- - -----------
    ---------------- ---- ---- ----- - ------ -----
    ---------------------- ----------- ------ --------------
    ----- --------------
  -

  ----- -------- ---------------------- -
    --- -
      ----- -------- - ----- ------------------------ --------- -----------
      -- --------- -- ----- -
        ------- -- --
        -------
      -
      ------- -- --
      ----- ------------------------ ----------
    - ----- ------- -
      ---------------------
      ------- -- --
    -
  -
-

-------------------------------

为模拟高并发环境,我们创建了1000个同时请求获取锁的节点。当所有请求完成后,会输出共花费多长时间完成测试。

测试结果表明,在1000个并发箭头下,仅有1~2次失败,且整个测试过程仅花费850毫秒,即每个节点的尝试获取锁的时间小于1ms,较为高效。

结论

通过本文,我们详细描述了Node.js中基于Redis的分布式锁的实现方案和设计原理。我们分别讨论了Redis SETNX操作和 BLPOP命令的实现方案,并对其进行了性能测试,以说明在高并发情况下该实现方案的高效性。

在生产环境中,我们还需要针对具体的业务场景细化调整实现方案,以达到更高的性能和更好的使用体验。但基于Redis的分布式锁机制已经成为了分布式系统中不可或缺的同步机制之一。掌握其实现方法和相关原理,也有助于我们在日常的系统设计和开发中更好地做好实现和应用。

来源:JavaScript中文网 ,转载请联系管理员! 本文地址:https://www.javascriptcn.com/post/670e1d065f551281025fb6d2


猜你喜欢

  • 如何在 Web Components 中使用 React、Vue、Angular 等前端框架

    Web Components 是指一组 W3C 标准,用于创建可重用的自定义元素和组件。它们是浏览器原生支持的,可用于在不同的前端框架和库之间实现组件复用。但是,在 Web Components 中使...

    12 天前
  • 掌握GraphQL中的数据取舍技巧

    GraphQL是一种数据查询语言和API协议,它可以让前端工程师非常灵活的控制数据的取舍,并且在客户端和服务器方面都很高效。在GraphQL中,我们需要了解一些数据取舍技巧,以便在前端工程中使用它们。

    12 天前
  • Performance Optimization 技巧:避免在 C++ 代码中使用多态

    在前端开发中,我们经常需要考虑性能优化的问题,而 C++ 是一门常用的编程语言,也是一个需要优化的领域。其中避免在 C++ 代码中使用多态就是一个重要的优化技巧。 什么是多态 多态是面向对象编程中的一...

    12 天前
  • RESTful API 设计中的异常处理方法

    在 RESTful API 的设计过程中,异常处理是一个非常重要的步骤。如果你的应用程序不能处理异常情况,那么用户与系统之间的交互将变得非常困难。因此,在 RESTful API 的开发中,我们必须谨...

    12 天前
  • Webpack 造成打包后资源路径错误的解决方案

    在前端开发中,Webpack 是一个非常重要的工具,它可以将多个文件打包成一个文件,在页面加载时只需要加载一个文件,从而提高网页加载速度。然而,在使用 Webpack 打包时,有时候会出现打包后资源路...

    12 天前
  • ECMAScript 2017 中新增的正则表达式命名捕获组及命名字符类

    ECMAScript 2017 中新增的正则表达式命名捕获组及命名字符类 在 ECMAScript 2017 中,正则表达式增加了命名捕获组和命名字符类,这为正则表达式的灵活性和可读性提供了巨大的提升...

    12 天前
  • ES11:函数的所有参数变成可选的

    ES11(也称为 ECMAScript 2020)是 JavaScript 语言的最新版本,它带来了许多新特性和语言改进,其中一个新特性是将函数的所有参数变成可选的。

    12 天前
  • 如何使用 Hapi 实现多租户应用程序

    随着云计算和 SaaS 购买模式的普及,多租户应用程序的需求越来越高。多租户应用程序是一种可以将多个租户的数据和业务逻辑分开存储和管理的应用程序。在前端开发中,使用 Hapi 框架可以很方便地实现多租...

    12 天前
  • 如何避免使用 Promise 链中出现的过度嵌套

    Promise 是 JavaScript 中处理异步编程的一种方式,凭借其优雅和强大的特性,得到了广大前端开发者的青睐。然而当 Promise 链中的嵌套层数增加时,代码可读性和维护性都会大大降低。

    12 天前
  • 如何使用 Koa2 实现自动化测试

    自动化测试在现代前端开发中扮演着重要的角色。它可以提高软件质量、降低开发成本和加速迭代速度。Koa2 是一个流行的 Node.js web 框架,本文将介绍如何使用 Koa2 实现自动化测试,帮助您在...

    12 天前
  • 减少 JavaScript 中展开运算符的使用以提高性能

    在前端开发中,展开运算符(spread operator)是一种非常方便的语法特性,它可以将数组或对象展开成单独的元素。然而,频繁使用展开运算符会对性能造成一定的影响,本文将详细介绍如何减少 Java...

    12 天前
  • Web Components VS 自定义组件

    Web Components 和自定义组件都是前端中非常常见的概念,特别是在大型项目中,这些组件往往占据了很大的比重。本文将比较这两种组件的优缺点,并提供实际示例代码。

    12 天前
  • 使用 GraphQL 优化前端应用程序的性能

    在开发现代前端应用程序时,性能是一个至关重要的因素。传统的 REST API 在处理复杂请求时可能会遇到一些性能瓶颈,这是 GraphQL 出现的原因之一。GraphQL 是一种查询语言,能够大大减少...

    12 天前
  • MongoDB 中的空间索引详解

    概述 MongoDB 是一个非常流行的 NoSQL 数据库,被广泛用于 Web 开发和移动应用程序。在应对许多实际应用案例时,我们需要对数据进行空间查询,这个时候,就需要使用 MongoDB 的空间索...

    12 天前
  • Deno 中的 WebSocket 错误:ERR_INVALID_HANDLE_STATE

    在 Deno 中使用 WebSocket 时,你可能会遭遇到一个错误:ERR_INVALID_HANDLE_STATE。这个错误奇怪地指出一个句柄处于非法状态,但实际上错误的原因比较深刻。

    12 天前
  • Node.js 中如何优雅地处理异步代码

    在 Node.js 中,由于 JavaScript 的异步特性,处理异步代码是一个常见的问题。在复杂的应用中,这可能会导致回调嵌套的情况,也称为回调地狱。为了避免回调地狱并让代码更易于阅读和维护,我们...

    12 天前
  • TypeScript 装饰器中范例讲解

    在 TypeScript 中,装饰器是一种特殊类型的声明,它可以被附加到类声明、方法、属性或参数上,以起到一定的修改行为或添加元数据的作用。本文将通过举例的方式,详细讲解 TypeScript 装饰器...

    12 天前
  • Serverless 之函数编程模型(FaaS、BaaS、CAP、IOC)

    什么是 Serverless? Serverless 是一种无服务器计算模型,它允许开发人员构建和运行应用程序和服务,而无需考虑服务器管理和维护。在 Serverless 模型中,资源和应用程序状态由...

    12 天前
  • Babel 编译后文件体积变大?几种解决方式

    背景 随着前端技术的不断发展,现代前端开发中经常使用编译器对高级语言(如 ES6 或 TypeScript)进行编译,以便在较早的浏览器中实现新功能。Babel 是最常见的 JavaScript 编译...

    12 天前
  • Kubernetes Postgres Operator 实践

    在现代化的技术世界中,云原生和 Kubernetes 已成为越来越受欢迎的选择,而 Postgres 数据库也是业界广泛使用的数据库之一。Kubernetes Postgres Operator 是一...

    12 天前

相关推荐

    暂无文章