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');
最后给上完整代码示例。