node.js Redis SETNX命令实现分布式锁解决超卖/定时任务重复执行问题

本文主要是介绍node.js Redis SETNX命令实现分布式锁解决超卖/定时任务重复执行问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

这篇关于node.js Redis SETNX命令实现分布式锁解决超卖/定时任务重复执行问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/653795

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

mybatis和mybatis-plus设置值为null不起作用问题及解决

《mybatis和mybatis-plus设置值为null不起作用问题及解决》Mybatis-Plus的FieldStrategy主要用于控制新增、更新和查询时对空值的处理策略,通过配置不同的策略类型... 目录MyBATis-plusFieldStrategy作用FieldStrategy类型每种策略的作

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

linux下多个硬盘划分到同一挂载点问题

《linux下多个硬盘划分到同一挂载点问题》在Linux系统中,将多个硬盘划分到同一挂载点需要通过逻辑卷管理(LVM)来实现,首先,需要将物理存储设备(如硬盘分区)创建为物理卷,然后,将这些物理卷组成... 目录linux下多个硬盘划分到同一挂载点需要明确的几个概念硬盘插上默认的是非lvm总结Linux下多