NestJS+Redis实现分布式锁

时间:2021-09-08
本文章向大家介绍NestJS+Redis实现分布式锁,主要包括NestJS+Redis实现分布式锁使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

高并发场景下容易出现的超卖问题(一张票卖给两个客户,或是库存卖成负数),一个常用的解决方法就是加锁。对于单机系统,内存级别的锁就足够应付(如c#中的lock);对于分布式系统Redis往往是一个常见的选项。当然,有一点要清楚的是:加锁有可能会影响代码执行效率,不是所有场景都适合加锁。
这里为了简化问题,假设有如下场景

请求到达后,代码会先查询是否还有库存,再创建订单,最后库存减一。 此处的并发问题是,如果在库存还剩一个的时候,两个请求同时到达第一步,此时都认为还有剩余,于是都创建了订单,最后库存减成了负数。

为了解决这个问题,以下示例在NestJS中使用Redis,为代码加锁,保证不会出现并发冲突。其逻辑是根据产品ID,在请求开始时,往redis中添加一条对应的记录,当请求处理结束,或是超时、异常时,删除该redis记录。其它请求进入时,也会查询redis中有无对应产品ID的记录,如果有就等待,直到解锁(记录被删除)或超时。

//新建nestjs项目,添加依赖
nest new redis-lock-demo

//这里是把redis作为数据库,记录产品剩余,非必须
npm install cache-manager
npm install -D @types/cache-manager
npm install --save cache-manager-redis-store

//以下两个依赖是实现了redis锁的插件
npm install nestjs-redis --save
npm install nestjs-simple-redis-lock

app.module.ts

import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ApiController } from './api/api.controller';
import * as redisStore from 'cache-manager-redis-store';

import { RedisModule } from 'nestjs-redis';
import { RedisLockModule } from 'nestjs-simple-redis-lock';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
    }),
    RedisModule.register({
      host: 'localhost',
      port: 6379,
    }),
    RedisLockModule.register({}),
  ],
  controllers: [AppController, ApiController],
  providers: [AppService],
})
export class AppModule {}

api.controller.ts

import { Controller, Get, CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { RedisLockService, RedisLock } from 'nestjs-simple-redis-lock';

@Controller('api')
export class ApiController {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    protected readonly lockService: RedisLockService, // inject RedisLockService
  ) {}

  @Get('long')
  /**
   * Automatically unlock after 1 min
   * Try again after 50ms if failed
   * The max times to retry is 200
   */
  @RedisLock('prod-001-lock', 1 * 60 * 1000, 50, 200)
  async getLongService(): Promise<string> {
    const balance = (await this.cacheManager.get('prod-001')) as number;

    if (balance <= 0) {
      return 'out of stock';
    }

    await new Promise((resolve) =>
      setTimeout(() => {
        console.log('do sonething long');
        resolve(1);
      }, 5000),
    );

    await this.cacheManager.set('prod-001', balance - 1, { ttl: 0 });

    return 'order created';
  }
}

api.controller.ts中的getLongService方法,先检查redis缓存中的产品剩余是否小于0,是则返回out of stock,负责等待5秒,库存减一,并返回order created。 如果不添加@RedisLock标签,两个客户端并发请求的话,会看到库存变为-1。 加了该标签则第一个请求处理的时候,第二个请求会等待。

当然,此处是在@RedisLock标签中hardcode了产品id,真实情况下需要动态指定,参见github地址。 代码示例如下:

await this.lockService.lock('test1', 2 * 60 * 1000, 50, 100);
this.lockService.unlock('test1');

最后给上完整代码示例

原文地址:https://www.cnblogs.com/Andy1982/p/15191698.html