Redis场景实战:分布式锁的概念与实践
在分布式系统中,多个进程或节点可能会同时访问共享资源,如数据库记录、文件或其他关键数据。如果没有适当的同步机制,这些并发访问可能会导致数据不一致或竞争条件。分布式锁是一种机制,用于确保在任何给定时间,只有一个进程能够访问共享资源,从而避免这些问题。
本文将演示如何使用 Node.js 和 TypeScript 实现一个带有可选自动重试机制的 Redis 分布式锁。
分布式锁的使用场景
分布式锁在许多场景中非常有用,以下是一些常见的使用场景:
- 分布式事务:在分布式系统中,多个服务可能需要协调一致地完成一系列操作。分布式锁可以确保这些操作按顺序执行,从而保证数据的一致性。
- 任务调度:在分布式任务调度系统中,确保同一任务不会被多个调度器同时执行。例如,定时任务系统需要确保某个任务在特定时间点只会被一个节点执行。
- 库存管理:在电商系统中,多个用户可能会同时购买同一商品。分布式锁可以确保库存扣减操作的原子性,避免超卖情况的发生。
- 缓存更新:当多个节点需要更新同一缓存时,分布式锁可以确保只有一个节点进行更新操作,从而避免缓存不一致的问题。
- 限流控制:在高并发场景下,分布式锁可以用于限流控制,确保某些关键操作不会被频繁调用,保护系统的稳定性。
基于 Redis 的分布式锁
Redis 是一种高性能的内存数据库,提供了一组原子操作,使其非常适合实现分布式锁。基于 Redis 的分布式锁通常依赖于以下几个关键点:
- 原子性:Redis 提供的命令(如
SETNX
和GETSET
)是原子的,确保多个客户端同时操作时不会产生竞态条件。 - 超时机制:锁应该有一个超时时间,以防止因进程崩溃而导致锁无法释放。
- 唯一性:每个锁应该有一个唯一的标识符,以确保只有持有锁的进程能够释放锁。
实现分布式锁
下面是一个使用 Node.js 和 TypeScript 实现分布式锁的完整示例,包含自动重试机制。
1. 安装依赖
首先,确保你已经安装了 Node.js 和 npm。然后安装所需的 npm 包:
npm install redis ioredis @types/redis @types/ioredis
2. 实现 RedisLock 类
创建一个 RedisLock
类来封装获取和释放锁的逻辑,并加入自动重试机制。
import { Redis } from "ioredis";
class RedisLock {
private client: Redis; // Redis 客户端实例
private lockKey: string; // 锁的键名
private lockValue: string; // 锁的唯一标识符
private ttl: number; // 锁的生存时间(毫秒)
constructor(client: Redis, lockKey: string, ttl: number = 30000) {
this.client = client;
this.lockKey = lockKey;
this.lockValue = Math.random().toString(36).slice(2); // 生成一个随机的唯一标识符
this.ttl = ttl;
}
// 尝试获取锁,带有可选的自动重试机制
async acquire(
retryDelay: number = 100,
maxRetries: number = 10
): Promise<boolean> {
let retries = 0;
while (maxRetries < 0 || retries < maxRetries) {
const result = await this.client.set(
this.lockKey,
this.lockValue,
"PX",
this.ttl,
"NX"
);
if (result === "OK") {
return true; // 成功获取锁
}
retries++;
await this.delay(retryDelay); // 等待一段时间后重试
}
return false; // 超过最大重试次数,获取锁失败
}
// 释放锁
async release(): Promise<boolean> {
// Lua 脚本,确保只有持有锁的进程才能释放锁
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
// 执行 Lua 脚本,传入锁的键和唯一标识符
const result = await this.client.eval(
script,
1,
this.lockKey,
this.lockValue
);
return result === 1; // 返回是否成功释放锁
}
// 延迟函数,用于等待一段时间
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
export default RedisLock;
使用示例
下面是如何使用带有可选重试机制的 RedisLock
类的示例:
import Redis from 'ioredis';
import RedisLock from './redisLock';
const redisClient = new Redis(); // 创建 Redis 客户端实例
async function main() {
const lock = new RedisLock(redisClient, 'my_resource_lock'); // 创建 RedisLock 实例
// 尝试获取锁,配置重试间隔为 200 毫秒,最大重试次数为 5 次
const acquired = await lock.acquire(200, 5);
if (acquired) {
console.log('Lock acquired!');
// 模拟处理共享资源
setTimeout(async () => {
const released = await lock.release(); // 释放锁
if (released) {
console.log('Lock released!');
} else {
console.log('Failed to release lock.');
}
}, 2000); // 模拟处理时间为 2 秒
} else {
console.log('Failed to acquire lock.');
}
}
main().catch((err) => {
console.error(err);
redisClient.quit(); // 出现错误时关闭 Redis 客户端
});
解释
-
可选重试机制:
acquire
方法现在接受两个可选参数:retryDelay
和maxRetries
。retryDelay
参数控制每次重试之间的等待时间,默认为 100 毫秒。maxRetries
参数控制最大重试次数,默认为 10 次。如果maxRetries
为负数(例如 -1),则表示无限重试。
-
灵活性:
- 使用者可以根据需要配置重试策略。例如,可以指定重试间隔为 200 毫秒,最大重试次数为 5 次。
- 如果希望无限重试,可以将
maxRetries
设置为负数(例如 -1)。
注意事项
- 本示例中的 TTL 设置为 30 秒(30000 毫秒)。根据实际需求,可以调整这个值。
- 在实际应用中,需要处理锁的自动续期(如果处理时间超过 TTL)和异常情况(如进程崩溃后锁未释放)。
通过这种方式,可以在分布式锁的实现中提供更加灵活的重试机制,满足不同场景下的需求。分布式锁在保证数据一致性和系统稳定性方面发挥着重要作用,特别是在高并发的分布式环境中。
转载自:https://juejin.cn/post/7394476656758554662