likes
comments
collection
share

Redis场景实战:分布式锁的概念与实践

作者站长头像
站长
· 阅读数 18

Redis场景实战:分布式锁的概念与实践

在分布式系统中,多个进程或节点可能会同时访问共享资源,如数据库记录、文件或其他关键数据。如果没有适当的同步机制,这些并发访问可能会导致数据不一致或竞争条件。分布式锁是一种机制,用于确保在任何给定时间,只有一个进程能够访问共享资源,从而避免这些问题。

本文将演示如何使用 Node.js 和 TypeScript 实现一个带有可选自动重试机制的 Redis 分布式锁。

分布式锁的使用场景

分布式锁在许多场景中非常有用,以下是一些常见的使用场景:

  1. 分布式事务:在分布式系统中,多个服务可能需要协调一致地完成一系列操作。分布式锁可以确保这些操作按顺序执行,从而保证数据的一致性。
  2. 任务调度:在分布式任务调度系统中,确保同一任务不会被多个调度器同时执行。例如,定时任务系统需要确保某个任务在特定时间点只会被一个节点执行。
  3. 库存管理:在电商系统中,多个用户可能会同时购买同一商品。分布式锁可以确保库存扣减操作的原子性,避免超卖情况的发生。
  4. 缓存更新:当多个节点需要更新同一缓存时,分布式锁可以确保只有一个节点进行更新操作,从而避免缓存不一致的问题。
  5. 限流控制:在高并发场景下,分布式锁可以用于限流控制,确保某些关键操作不会被频繁调用,保护系统的稳定性。

基于 Redis 的分布式锁

Redis 是一种高性能的内存数据库,提供了一组原子操作,使其非常适合实现分布式锁。基于 Redis 的分布式锁通常依赖于以下几个关键点:

  1. 原子性:Redis 提供的命令(如 SETNXGETSET)是原子的,确保多个客户端同时操作时不会产生竞态条件。
  2. 超时机制:锁应该有一个超时时间,以防止因进程崩溃而导致锁无法释放。
  3. 唯一性:每个锁应该有一个唯一的标识符,以确保只有持有锁的进程能够释放锁。

实现分布式锁

下面是一个使用 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 客户端
});

解释

  1. 可选重试机制

    • acquire 方法现在接受两个可选参数:retryDelaymaxRetries
    • retryDelay 参数控制每次重试之间的等待时间,默认为 100 毫秒。
    • maxRetries 参数控制最大重试次数,默认为 10 次。如果 maxRetries 为负数(例如 -1),则表示无限重试。
  2. 灵活性

    • 使用者可以根据需要配置重试策略。例如,可以指定重试间隔为 200 毫秒,最大重试次数为 5 次。
    • 如果希望无限重试,可以将 maxRetries 设置为负数(例如 -1)。

注意事项

  • 本示例中的 TTL 设置为 30 秒(30000 毫秒)。根据实际需求,可以调整这个值。
  • 在实际应用中,需要处理锁的自动续期(如果处理时间超过 TTL)和异常情况(如进程崩溃后锁未释放)。

通过这种方式,可以在分布式锁的实现中提供更加灵活的重试机制,满足不同场景下的需求。分布式锁在保证数据一致性和系统稳定性方面发挥着重要作用,特别是在高并发的分布式环境中。

转载自:https://juejin.cn/post/7394476656758554662
评论
请登录