node.js 分布式锁看这篇就够用了

Redis SETNX 命令背后的原理探究

当然,让我们通过一个简单的例子,使用 Redis CLI(命令行界面)来模拟获取锁和释放锁的过程。 在此示例中

  1. 获取锁:
# 首先,设置锁密钥的唯一值和过期时间(秒)
127.0.0.1:6379> SET lock:tcaccount_1234 unique_value NX EX 3
OK

这里,“unique_value”是与锁关联的唯一标识符的占位符(生产环境UUID,随字符串),“EX 3”将过期时间设置为 3 秒

  1. 在另一个会话或请求中检查并获取锁:
# 其次,检查锁key是否存在,不存在则获取锁
127.0.0.1:6379> SET lock:tcaccount_1234 unique_value NX EX 3
(nil)

第二次尝试返回 nil,因为锁已经存在。 在真实的应用程序中,您将检查结果,如果结果为零,您可能会转到下一个帐户或等待并重试。

  1. 释放锁:
# 通过删除锁定密钥来解除锁定
127.0.0.1:6379> DEL lock:tcaccount_1234
(integer) 1

The DEL 命令用于删除锁键,有效释放锁。 返回的整数值 1 表示删除了一个键。

请注意,这是一个简化的示例,在现实场景中,您通常会使用脚本(例如 Lua 脚本)来使锁的获取和释放原子化,从而防止竞争条件。 这里的示例旨在说明使用 Redis 命令进行锁定的基本原理。

Node.js 程序中集成

node -v # v16.20.2
npm install redis # 笔者版本"redis": "^4.2.0"

node.js redis client.eval() 方法lua脚本如何正确传参

// redis version 4x:
let result = await client.eval('return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}', {keys: ['key1', 'key2'],arguments: ['first', 'second']
}); 
//result =  [ 'key1', 'key2', 'first', 'second' ]// redis version 3x:
v3Client.eval('return KEYS[1]', '1', 'key', (err, reply) => {console.log(reply); // 'key'
});
v3Client.eval('return KEYS[1]', '0', 'argument', (err, reply) => {console.log(reply); // 'argument'
});

请注意redis 驱动依赖库版本选择对应的语法

加锁实现

错误加锁方式一分步设置值和过期时间

在分布式加锁中,设置键值和设置过期时间应该是原子操作,以确保在设置键值的同时,也设置了过期时间。如果将这两步操作分开,可能会导致在设置键值后,还未来得及设置过期时间时,其他进程可能已经获取了锁。

下面是你的 JavaScript 代码拆分为两步的示例,并添加了一些中文注释和错误演示:

// 第一步:设置键值
const setResult = await client.set(lockKey, uniqueValue);// 第二步:设置过期时间
const expireResult = await client.expire(lockKey, expireTime);// 检查结果
if (setResult === 'OK' && expireResult === 1) {console.log(`[s] 已获取锁 ${resourceKey}`);return true;
} else {console.log(`[x] 无法获取锁 ${resourceKey}`);return false;
}

这里使用 client.set 来设置键值,然后使用 client.expire 来设置过期时间。请注意,这两个操作是分开的,因此在设置键值后,还需要等待过期时间的设置。这样的分步操作可能导致在设置键值后,其他进程可能已经获取了锁,因为过期时间还未来得及设置。

错误加锁方式二
 const result =   await client.setEx(lockKey, expireTime, uniqueValue);if (result === 'OK') {console.log(`[s] 已获取锁 ${resourceKey}`);return true;} else {console.log(`[x] 无法获取锁 ${resourceKey}`);return false;}

如图所示怎样加锁并不是原子性
java go 语言中这种方式可行,但是时在 node.js redis 4.2.0 中并不能避免并发问题(见下gif 动图演示)

正确的 Lua脚本用于原子获取锁
        // 锁的键和值const lockKey = `lock:${resourceKey}`;// Lua脚本用于原子获取锁const luaScript = `if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) thenreturn 1elsereturn 0end`;// 执行Lua脚本const result = await client.eval(luaScript, {keys: [lockKey],arguments: [uniqueValue, `${expireTime}`]});if (result === 1) {console.log(`[s] 已获取锁 ${resourceKey}`);return true;} else {console.log(`[x] 无法获取锁 ${resourceKey}`);return false;}}

请添加图片描述

释放锁的实现

释放锁时需要验证value值,也就是说我们在获取锁的时候需要设置一个value,不能直接用del key这种粗暴的方式,因为直接del key任何客户端都可以进行解锁了,所以解锁时,我们需要判断锁是否是自己的,基于value值来判断,代码如下

 
/*** 释放锁* @param resourceKey 资源键名* @param uniqueValue 唯一值,用于验证锁的所有者(建议:UUID)* @returns 是否成功释放锁*/async function unlock(resource, uniqueValue) {const lockKey = `lock:${resource}`;const luaScript = `if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0end`;const result = await client.eval(luaScript, {keys: [lockKey],arguments: [uniqueValue]});if (result === 1) {console.log('[s] 锁释放成功');} else {console.log('[x] 锁释放失败,可能锁已经被其他客户端更新');}}

在释放锁的操作中,使用 uniqueValue 的唯一值是为了确保只有持有相应唯一值的客户端才能成功释放锁。这是为了防止其他客户端错误地释放了不属于它们的锁。

具体来说,释放锁的 Lua 脚本中的这部分逻辑:

if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end

这段脚本首先检查锁的当前持有者是否与传入的 uniqueValue 相匹配。如果匹配,说明当前调用释放锁的客户端是锁的所有者,然后执行 DEL 命令删除锁。如果不匹配,则返回 0,表示释放锁失败。

使用 uniqueValue 的好处是:

  1. 确保只有锁的所有者才能释放锁: 持有相应 uniqueValue 的客户端才能成功释放锁。如果其他客户端尝试使用不同的 uniqueValue 释放锁,Lua 脚本会拒绝操作,保护了锁的所有权。

  2. 防止误释放: 避免了其他客户端误操作释放了不属于它们的锁。如果不使用唯一值,任何客户端都可以尝试释放锁,这可能导致竞争条件和不一致性。

在分布式系统中,确保释放锁的操作是安全和可靠的是至关重要的,使用唯一值是一种有效的方式。通常,可以使用唯一标识符(如 UUID)作为 uniqueValue,以确保其唯一性。

应用场景

在这里插入图片描述
多台机器定时任务重复执行(如:日终对账,0点0分只有一个任务去工作,其他没拿到锁跳过了任务)
订单超卖(如:操作同一商品库存时,保证并发下唯一个任务拿到库存数去做扣库存,创建订单操作)

完整脚本如下

const {createClient} = require('redis');
const {generateUUID} = require("../models/utl");
(async ()=> {const client = await createClient().on('error', err => console.log('Redis Client Error', err)).connect();async function lock(resourceKey, uniqueValue, expireTime = 10) {// 锁的键和值const lockKey = `lock:${resourceKey}`;/*   const result =   await client.setEx(lockKey, expireTime, uniqueValue);if (result === 'OK') {console.log(`[s] 已获取锁 ${resourceKey}`);return true;} else {console.log(`[x] 无法获取锁 ${resourceKey}`);return false;}
*/// Lua脚本用于原子获取锁const luaScript = `if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) thenreturn 1elsereturn 0end`;// 执行Lua脚本const result = await client.eval(luaScript, {keys: [lockKey],arguments: [uniqueValue, `${expireTime}`]});if (result === 1) {console.log(`[s] 已获取锁 ${resourceKey}`);return true;} else {console.log(`[x] 无法获取锁 ${resourceKey}`);return false;}}async function unlock(resource, uniqueValue) {const lockKey = `lock:${resource}`;const luaScript = `if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0end`;const result = await client.eval(luaScript, {keys: [lockKey],arguments: [uniqueValue]});if (result === 1) {console.log('[s] 锁释放成功');} else {console.log('[x] 锁释放失败,可能锁已经被其他客户端更新');}}async function exampleUsage(resource) {const uniqueValue = generateUUID();const isLockAcquired = await lock(resource, uniqueValue);if (isLockAcquired) {try {// 在这里执行受锁保护的代码// 模拟一些处理时间await new Promise(resolve => setTimeout(resolve, 5000));} finally {// 最后释放锁unlock(resource, uniqueValue);}} else {console.log('[x] 未获取锁。 另一个进程可能正在持有锁。');}}const resourcePk = 'account_id123'let taskList = []for (let i = 0; i < 10; i++) {taskList.push( exampleUsage(resourcePk))}//并发拿同一账号await Promise.all(taskList);await new Promise(resolve => setTimeout(resolve, 6000));//测试重新获取锁await exampleUsage(resourcePk);})()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/652128.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字三角形(很经典的动态规划问题)

给定一个如下图所示的数字三角形&#xff0c;从顶部出发&#xff0c;在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点&#xff0c;一直走到底层&#xff0c;要求找出一条路径&#xff0c;使路径上的数字的和最大。 73 88 1 02 7 4 4 4 5 2 6 …

第2章-神经网络的数学基础——python深度学习

第2章 神经网络的数学基础 2.1 初识神经网络 我们来看一个具体的神经网络示例&#xff0c;使用 Python 的 Keras 库 来学习手写数字分类。 我们这里要解决的问题是&#xff0c; 将手写数字的灰度图像&#xff08;28 像素28 像素&#xff09;划分到 10 个类别 中&#xff08;0…

基于多种CNN模型在清华新闻语料分类效果上的对比

该实验项目目录如图&#xff1a; 1、 模型 1.1. TextCNN # coding: UTF-8 import torch import torch.nn as nn import torch.nn.functional as F import numpy as npclass Config(object):"""配置参数"""def __init__(self, dataset, embedd…

【C++类与对象(上)】

C类与对象(上&#xff09; 1.面向过程和面向对象初步认识2.类的引入3.类的定义4.类的访问限定符及封装4.1 访问限定符4.2 封装 5.类的作用域6.类的实例化7.类的对象大小的计算7.1如何计算类对象的大小7.2 类对象的存储方式猜测7.3结构体内存对齐规则 8.类成员函数的this指针8.1…

Java多线程基础-18:线程安全的集合类与ConcurrentHashMap

Java标准库提供了很多集合类&#xff0c;但有一些集合类是线程不安全的&#xff0c;也就是说&#xff0c;在多线程环境下可能会出问题的。常用的ArrayList&#xff0c;LinkedList&#xff0c;HashMap&#xff0c;PriorityQueue等都是线程不安全的&#xff08;Vector, Stack, Ha…

Android创建工程

语言选择Java&#xff0c;我用的Java 最小SDK&#xff1a;就是开发的APP支持的最小安卓版本 Gradle 是一款Google 推出的基于 JVM、通用灵活的项目构建工具&#xff0c;支持 Maven&#xff0c;JCenter 多种第三方仓库;支持传递性依赖管理、废弃了繁杂的xml 文件&#xff0c;转而…

关于ArcGIS的Update更新工具的疑问

Update更新工具官方帮助文件解释如下&#xff1a; 但是根据这个插图很让人疑惑&#xff0c;输入要素是蓝色&#xff0c;更新要素是黄色&#xff0c;输出要素为绿色&#xff0c;而且全部是绿色。我一直以为是与更新要素相交&#xff08;被包含切割&#xff09;的哪些输入要素都被…

【常用工具】7-Zip 解/压缩软件——基本使用方法

在实际日常工作或项目中&#xff0c;经常会遇到需要在window操作系统上压缩文件&#xff0c;在Linux操作系统上解压缩的场景&#xff0c;一款实用的压缩软件迫在眉睫&#xff0c;经过实际使用总结&#xff0c;7-Zip可以很好的解决很多压缩和解压缩问题&#xff0c;其基本使用方…

WordPress如何自定义日期和时间格式?附PHP日期和时间格式字符串

WordPress网站在很多地方都需要用到日期和时间&#xff0c;那么我们应该在哪里设置日期和时间呢&#xff1f;又如何自定义日期和时间格式呢&#xff1f;下面boke112百科就跟大家一起来学习一下PHP标准化的日期和时间格式字符串。 特别说明&#xff1a;格式字符是标准化的&#…

canvas绘制旋转的大风车

查看专栏目录 canvas实例应用100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

LCweekly-game

ExScorecomplete situation1220717/719(解答错误)30523/537(超时,弱智题已AC)40 有用的是Ex2和Ex4 Ex2 my solution class Solution { public://calculate xs l-time 幂乘int jiecheng(int x,int l){int zx;for(int i0;i<l;i){if(z>pow(10,4.5))return 0;zz*z;}return…

C#算法(11)—求三个点构成圆的圆心坐标和半径

前言 我们在上位机开发领域也经常会碰到根据三个点求出圆的圆形、半径等信息的场景,本文就是详细的介绍如何根据三个点使用C#代码求出三点构成的圆的圆心坐标、圆半径、三点构成的圆弧的角度。 1、3点求圆分析 A、B、C三个点都是圆上的坐标点,过向量AB做中垂线,过向量AC做…

What is `@Scheduled` does?

Scheduled 是Spring框架中用于定时任务调度的注解&#xff0c;它允许我们在类的方法上声明一个方法作为定时任务&#xff0c;由Spring容器统一管理和执行。使用此注解后&#xff0c;Spring会根据注解中的属性配置&#xff0c;按照指定的时间规则自动调用该方法。 public class…

文心一言 VS ChatGPT :谁是更好的选择?

前言 目前各种大模型、人工智能相关内容覆盖了朋友圈已经各种媒体平台&#xff0c;对于Ai目前来看只能说各有千秋。GPT的算法迭代是最先进的&#xff0c;但是它毕竟属于国外产品&#xff0c;有着网络限制、注册限制、会员费高昂等弊端&#xff0c;难以让国内用户享受。文心一言…

2023年度AI盘点 AIGC|AGI|ChatGPT|人工智能大模型

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 2023年是人工智能大语言模型大爆发的一年&#xff0c;一些概念和英文缩写也在这一年里集中出现&#xff0c;很容易混淆&#xff0c;甚至把人搞懵。 文章目录 前言01 《ChatGPT 驱动软件开…

使用一个定时器(timer_fd)管理多个定时事件

使用一个定时器(timer_fd)管理多个定时事件 使用 timerfd_xxx 系列函数可以很方便的与 select、poll、epoll 等IO复用函数相结合&#xff0c;实现基于事件的定时器功能。大体上有两种实现思路&#xff1a; 为每个定时事件创建一个 timer_fd&#xff0c;绑定对应的定时回调函数…

QEMU源码全解析41 —— Machine(11)

接前一篇文章&#xff1a;QEMU源码全解析40 —— Machine&#xff08;10&#xff09; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; 时间过去了几…

go语言(二十一)---- channel的关闭

channel不像文件一样需要经常去关闭&#xff0c;只有当你确实没有任何发送数据了&#xff0c;或者你想显示的结束range循环之类的&#xff0c;才去关闭channel。关闭channel后&#xff0c;无法向channel再发送数据&#xff0c;&#xff08;引发pannic错误后&#xff0c;导致接收…

Linux编译实时内核和打补丁

目录 1.Linux内核2.实时内核3.编译实时内核3.1 准备3.2 获取内核源码3.3 编译3.4 设置GRUB确保启动到实时内核 4.给内核打补丁5.安装新的内核 1.Linux内核 https://github.com/torvalds/linux Linux内核是Linux操作系统的核心部分&#xff0c;它是操作系统的基本组成部分&…

spring整合mybatis的底层原理

spring整合mybatis的底层原理 原理&#xff1a; FactoryBean的自定义对象jdk动态代理Mapper接口对象 一、手写一个spring集成mybatis 目录结构&#xff1a; 1.1 入口类 public class Test {public static void main(String[] args) {AnnotationConfigApplicationContext co…