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

Redis SETNX 特性

当然,让我们通过一个简单的例子,使用 Redis CLI(命令行界面)来模拟获取锁和释放锁的过程。 在此示例中,我将使用键“lock:tcaccount_[pk]”和“status:tcaccount_[pk]”分别表示锁定键和状态键。

  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"

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

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' ]

加锁实现

   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]) then
            return 1
          else
            return 0
          end
        `;

        // 执行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] then
            return redis.call("DEL", KEYS[1])
          else
            return 0
          end
        `;
        const result = await client.eval(luaScript, {
   
            keys: [lockKey],
            arguments: [uniqueValue]
        });

        if (result === 1) {
   
            console.log('[s] 锁释放成功');
        } else {
   
            console.log('[x] 锁释放失败,可能锁已经被其他客户端更新');
        }
    }

应用场景

在这里插入图片描述
多台机器定时任务
订单超卖

完整脚本如下

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]) then
            return 1
          else
            return 0
          end
        `;

        // 执行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] then
            return redis.call("DEL", KEYS[1])
          else
            return 0
          end
        `;
        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);

})()

最近更新

  1. TCP协议是安全的吗?

    2024-01-28 06:50:01       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-28 06:50:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-28 06:50:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-28 06:50:01       18 阅读

热门阅读

  1. WPF的ViewBox控件

    2024-01-28 06:50:01       34 阅读
  2. docker-compose离线安装

    2024-01-28 06:50:01       34 阅读
  3. Debian 12.x apt方式快速部署LNMP

    2024-01-28 06:50:01       25 阅读
  4. 03 创建图像窗口的几种方式

    2024-01-28 06:50:01       34 阅读
  5. LeetCode-题目整理【12】:N皇后问题--回溯算法

    2024-01-28 06:50:01       39 阅读
  6. 【从浅到深的算法技巧】初级排序算法 上

    2024-01-28 06:50:01       35 阅读