【NestJs】基于Redlock装饰器分布式锁设计与实现

前言

在分布式系统中,多个服务实例同时操作共享资源时,如何保证数据一致性是一个经典问题。传统的单机锁(如 synchronizedReentrantLock)在分布式环境下失效,我们需要分布式锁来解决。

本文将介绍如何在 NestJS 中设计并实现一个声明式分布式锁 组件,通过 @RedLock 装饰器实现无侵入式的并发控制。

一、为什么需要分布式锁?

1.1 问题场景

css 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    典型场景:秒杀抢购                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  用户 A ────┐                                                   │
│             │                                                   │
│  用户 B ────┼───► 服务实例 1 ────┐                              │
│             │                    │                              │
│  用户 C ────┼───► 服务实例 2 ────┼───► 数据库(库存 = 1)        │
│             │                    │                              │
│  用户 D ────┼───► 服务实例 3 ────┘                              │
│             │                                                   │
│             └─── 问题:三个实例同时查询库存,都认为有库存          │
│                  导致超卖!                                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1.2 分布式锁的核心诉求

特性 说明
互斥性 同一时刻只有一个客户端持有锁
防死锁 锁必须有过期时间,防止持有者崩溃后无法释放
高可用 锁服务不能成为单点故障
可重入 同一线程可多次获取同一把锁

二、Redlock 算法简介

Redlock(Redis Distributed Lock)是 Redis 作者 Antirez 提出的分布式锁算法,核心思想是:

向多个独立的 Redis 节点请求加锁,只有当在大多数节点上都成功获取锁时,才算加锁成功。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Redlock 算法流程                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  客户端                                                         │
│    │                                                            │
│    ├─── 1. 获取当前时间戳                                        │
│    │                                                            │
│    ├─── 2. 依次向 N 个 Redis 节点请求加锁                         │
│    │         ┌─────────────────────────────────────┐            │
│    │         │ Redis-1  │ Redis-2  │ Redis-3      │            │
│    │         │   ✓      │   ✓      │   ✗         │            │
│    │         └─────────────────────────────────────┘            │
│    │                                                            │
│    ├─── 3. 计算获取锁消耗的时间                                   │
│    │                                                            │
│    ├─── 4. 有效锁 = 加锁成功数 > N/2 且 消耗时间 < 锁TTL          │
│    │                                                            │
│    └─── 5. 加锁成功 / 失败则向所有节点发释放请求                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三、模块设计概览

我们设计的 Redlock 模块包含以下核心组件:

ruby 复制代码
libs/redlock/src/
├── redlock.interface.ts          # 类型定义
├── redlock.module-definition.ts  # 动态模块构建器
├── redlock.service.ts            # 核心服务(继承 Redlock)
├── redlock.decorator.ts          # @RedLock 声明式装饰器
├── redlock.module.ts             # 模块定义
└── index.ts                      # 导出

四、核心实现解析

4.1 类型定义

首先定义模块配置的接口:

typescript 复制代码
// redlock.interface.ts
import { RedisOptions } from "ioredis";
import { Settings } from "redlock";

export interface RedlockModuleOptions {
    // 支持单节点或多节点 Redis 配置
    redisClient: RedisOptions | RedisOptions[]
    // Redlock 高级配置
    settings?: Partial<Settings>
}

4.2 动态模块构建

使用 NestJS 的 ConfigurableModuleBuilder 实现动态模块配置:

typescript 复制代码
// redlock.module-definition.ts
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { RedlockModuleOptions } from './redlock.interface';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = 
  new ConfigurableModuleBuilder<RedlockModuleOptions>()
    .setClassMethodName('forRoot')
    .setFactoryMethodName('createRedlockOptions')
    .setExtras({
      isGlobal: true, // 默认全局模块
    }, (definition, extras) => ({
      ...definition,
      isGlobal: extras.isGlobal,
    }))
    .build();

设计亮点:

  • 使用 ConfigurableModuleBuilder 简化动态模块创建
  • 默认设置为全局模块,避免重复导入
  • 通过 MODULE_OPTIONS_TOKEN 实现配置注入

4.3 服务层实现

RedlockService 继承 Redlock,封装 Redis 客户端初始化:

typescript 复制代码
// redlock.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { MODULE_OPTIONS_TOKEN } from './redlock.module-definition';
import { RedlockModuleOptions } from './redlock.interface';
import Redlock from 'redlock';
import Client from "ioredis";

@Injectable()
export class RedlockService extends Redlock {
    constructor(@Inject(MODULE_OPTIONS_TOKEN) options: RedlockModuleOptions) {
        // 将单个或多个配置统一为数组,然后 map 创建客户端
        const clients = [options.redisClient]
            .flat()
            .map(config => new Client(config));
        
        super(clients, options.settings);
    }
}

代码解析:

  • [options.redisClient].flat() 巧妙处理单节点/多节点配置
  • 继承 Redlock 使服务具备完整的锁操作能力
  • 通过依赖注入获取配置,符合 NestJS 设计原则

4.4 模块定义

typescript 复制代码
// redlock.module.ts
import { Global, Module } from '@nestjs/common';
import { RedlockService } from './redlock.service';
import { ConfigurableModuleClass } from './redlock.module-definition';

@Module({
  providers: [RedlockService],
  exports: [RedlockService],
})
export class RedlockModule extends ConfigurableModuleClass {}

使用方式:

typescript 复制代码
// app.module.ts
import { RedlockModule } from '@app/redlock';

@Module({
  imports: [
    RedlockModule.forRoot({
      redisClient: {
        host: 'localhost',
        port: 6379,
      },
      settings: {
        // 锁默认过期时间
        driftFactor: 0.01,
        retryCount: 3,
        retryDelay: 200,
      },
    }),
  ],
})
export class AppModule {}

五、声明式装饰器实现(核心亮点)

这是整个模块最精妙的部分,通过装饰器 + Proxy 实现声明式锁控制。 除此之外当然还有很多种实现方式,如Interceptor、Injector等等方案 这里降低代码耦合度我决定使用ModuleRef的特性在运行时获取到RedlockService去获取锁(不过对比手动注入RedlockService方案使用ModuleRef会略微增加一点性能消耗可忽略不计)。

5.1 装饰器设计

typescript 复制代码
// redlock.decorator.ts
import { HttpException, HttpStatus } from "@nestjs/common";
import { RedlockService } from "./redlock.service";
import { ExecutionError, Settings, Lock } from "redlock";
import { ModuleRef } from "@nestjs/core";

export const RedLock = (
  key: string | string[],   // 锁的 key,支持多个
  ttl: number,              // 锁过期时间(毫秒)
  settings?: Partial<Settings>  // 可选的高级配置
) => {
    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        const originalMethod = descriptor.value;
        
        // 校验装饰目标必须是方法
        if (!descriptor || typeof descriptor.value !== 'function') {
            throw new Error(
              `@RedLock 装饰器只能应用于方法。属性 ${String(propertyKey)} 不是一个方法。`,
            );
        }
        
        // 注入 ModuleRef 用于运行时获取 RedlockService
        Inject(ModuleRef)(target, ModuleRef.name);
        //Inject(RedlockService)(target,ModuleRef.name) 同时也可以手动注入 ,但无法throw异常提示RedlockService 未注入或者模块未配置等
        // 使用 Proxy 代理方法调用
        descriptor.value = new Proxy(originalMethod, {
            apply: async (target, thisArg, argumentsList) => {
                // 运行时获取 RedlockService 实例
                const moduleRef = thisArg[ModuleRef.name] as ModuleRef;
                const redlockService = moduleRef.get(RedlockService, { 
                  strict: false 
                });
                
                if (!redlockService) {
                    throw new Error(
                      '@RedLock 装饰器需要 RedlockService 但未注入,' +
                      '请检查 RedLockModule 是否正确配置'
                    );
                }
                
                let lock: Lock | undefined;
                try {
                    // 获取锁
                    lock = await redlockService.acquire(
                      Array.isArray(key) ? key : [key], 
                      ttl, 
                      settings
                    );
                    
                    // 执行原始方法
                    return await Reflect.apply(target, thisArg, argumentsList);
                    
                } catch (error) {
                    // 锁获取失败处理
                    if (error instanceof ExecutionError) {
                        throw new HttpException(
                          '业务繁忙,请稍后再试!', 
                          HttpStatus.CONFLICT
                        );
                    }
                    throw error;
                    
                } finally {
                    // 确保锁被释放
                    if (lock) {
                        await lock.release().catch(console.error);
                    }
                }
            },
        });
        
        return descriptor;
    };
};

5.2 工作流程图

typescript 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    @RedLock 装饰器执行流程                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  方法调用                                                        │
│    │                                                            │
│    ▼                                                            │
│  ┌─────────────────────────────────────────┐                   │
│  │ Proxy.apply 拦截                         │                   │
│  │                                          │                   │
│  │  1. 通过 ModuleRef 获取 RedlockService   │                   │
│  │  2. 调用 acquire() 获取分布式锁          │                   │
│  │     │                                    │                   │
│  │     ├─── 成功 ──► 执行原始方法           │                   │
│  │     │                                    │                   │
│  │     └─── 失败 ──► 抛出业务异常           │                   │
│  │                                          │                   │
│  │  3. finally 释放锁                       │                   │
│  └─────────────────────────────────────────┘                   │
│    │                                                            │
│    ▼                                                            │
│  返回结果                                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 为什么使用 ModuleRef 而不是直接注入?

typescript 复制代码
// 方案一:直接注入(不推荐)
// 问题:需要提前在目标类中注入 RedlockService,侵入性强

// 方案二:通过 ModuleRef 动态获取(推荐)
// 优点:无需提前注入,运行时按需获取,减少耦合

六、实际使用示例

6.1 基础用法

typescript 复制代码
// user.service.ts
import { RedLock } from '@app/redlock';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
    
    // 单个锁 key
    @RedLock('user:deduct:balance', 5000)  // 5秒过期
    async deductBalance(userId: number, amount: number) {
        // 业务逻辑,此时已持有分布式锁
        // 不会出现并发扣款问题
    }
    
    // 多个锁 key(同时锁多个资源)
    @RedLock(['order:create', 'inventory:check'], 10000)
    async createOrder(productId: number, userId: number) {
        // 同时锁定订单创建和库存检查
        // 防止超卖
    }
}

6.2 与事务结合

typescript 复制代码
// withdraw.service.ts
@Injectable()
export class WithdrawService {
    constructor(private prisma: PrismaService) {}
    
    @RedLock('withdraw:process', 10000)
    async processWithdraw(userId: number, amount: number) {
        return this.prisma.$transaction(async (tx) => {
            // 1. 查询用户余额
            const user = await tx.user.findUnique({ where: { id: userId } });
            
            // 2. 检查余额是否足够
            if (user.balance < amount) {
                throw new BadRequestException('余额不足');
            }
            
            // 3. 扣除余额
            await tx.user.update({
                where: { id: userId },
                data: { balance: { decrement: amount } }
            });
            
            // 4. 创建提现记录
            return tx.withdrawRecord.create({
                data: { userId, amount, status: 'pending' }
            });
        });
    }
}

七、与传统方案对比

7.1 传统手动加锁方式

typescript 复制代码
// 传统方式:手动管理锁生命周期
async deductBalance(userId: number, amount: number) {
    let lock;
    try {
        // 手动获取锁
        lock = await this.redlockService.acquire(['user:balance'], 5000);
        
        // 业务逻辑
        await this.doSomething();
        
    } catch (error) {
        if (error instanceof ExecutionError) {
            throw new HttpException('请稍后重试', HttpStatus.CONFLICT);
        }
        throw error;
    } finally {
        // 手动释放锁
        if (lock) await lock.release();
    }
}

问题:

  • 代码冗余,每个需要锁的方法都要重复 try-catch-finally
  • 容易遗漏释放锁,导致死锁
  • 锁 key 管理分散

7.2 声明式装饰器方式

typescript 复制代码
// 声明式:一行注解搞定
@RedLock('user:balance', 5000)
async deductBalance(userId: number, amount: number) {
    // 纯粹的业务逻辑
    await this.doSomething();
}

优势:

对比项 传统方式 装饰器方式
代码量
可读性 业务逻辑被锁代码包围 清晰直观
维护性 容易遗漏释放 自动释放
复用性 每次都要写 一处定义处处可用

八、总结

8.1 核心设计思想

javascript 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    设计思想总结                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. 动态模块配置                                                 │
│     └── ConfigurableModuleBuilder 支持灵活配置                   │
│                                                                 │
│  2. 继承优于组合                                                 │
│     └── RedlockService 继承 Redlock,保留完整功能                │
│                                                                 │
│  3. 装饰器 + Proxy                                               │
│     └── 声明式编程,无侵入式增强                                  │
│                                                                 │
│  4. ModuleRef 动态依赖                                           │
│     └── 运行时获取,减少耦合                                      │
│                                                                 │
│  5. 统一异常处理                                                 │
│     └── 将技术异常转换为业务异常                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

8.3 注意事项

  1. 锁的粒度:锁 key 设计要合理,粒度过大会影响并发性能
  2. TTL 设置:过期时间要大于业务执行时间,但要合理控制
  3. 异常处理:获取锁失败要有降级策略
  4. Redis 集群:生产环境建议使用多节点提高可用性

参考资料:

相关推荐
㳺三才人子6 小时前
初探 Flask
后端·python·flask·html
星栈独行6 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.6 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶7 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel8 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记9 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒10 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰10 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程