网站域名解析时间,网页设计的版式有哪些,网站详情页链接怎么做,网站开发温州Redis SETNX 命令背后的原理探究
当然#xff0c;让我们通过一个简单的例子#xff0c;使用 Redis CLI#xff08;命令行界面#xff09;来模拟获取锁和释放锁的过程。 在此示例中
获取锁:
# 首先#xff0c;设置锁密钥的唯一值和过期时间(秒)
127.0.0.1:6379 SET …Redis SETNX 命令背后的原理探究
当然让我们通过一个简单的例子使用 Redis CLI命令行界面来模拟获取锁和释放锁的过程。 在此示例中
获取锁:
# 首先设置锁密钥的唯一值和过期时间(秒)
127.0.0.1:6379 SET lock:tcaccount_1234 unique_value NX EX 3
OK这里“unique_value”是与锁关联的唯一标识符的占位符(生产环境UUID,随字符串)“EX 3”将过期时间设置为 3 秒
在另一个会话或请求中检查并获取锁
# 其次检查锁key是否存在不存在则获取锁
127.0.0.1:6379 SET lock:tcaccount_1234 unique_value NX EX 3
(nil)第二次尝试返回 nil因为锁已经存在。 在真实的应用程序中您将检查结果如果结果为零您可能会转到下一个帐户或等待并重试。
释放锁:
# 通过删除锁定密钥来解除锁定
127.0.0.1:6379 DEL lock:tcaccount_1234
(integer) 1The DEL 命令用于删除锁键有效释放锁。 返回的整数值 1 表示删除了一个键。
请注意这是一个简化的示例在现实场景中您通常会使用脚本例如 Lua 脚本来使锁的获取和释放原子化从而防止竞争条件。 这里的示例旨在说明使用 Redis 命令进行锁定的基本原理。
Node.js 程序中集成
node -v # v16.20.2
npm install redis # 笔者版本redis: ^4.2.0node.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 的好处是 确保只有锁的所有者才能释放锁 持有相应 uniqueValue 的客户端才能成功释放锁。如果其他客户端尝试使用不同的 uniqueValue 释放锁Lua 脚本会拒绝操作保护了锁的所有权。 防止误释放 避免了其他客户端误操作释放了不属于它们的锁。如果不使用唯一值任何客户端都可以尝试释放锁这可能导致竞争条件和不一致性。
在分布式系统中确保释放锁的操作是安全和可靠的是至关重要的使用唯一值是一种有效的方式。通常可以使用唯一标识符如 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_id123let 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);})()