72 lines
1.6 KiB
TypeScript
72 lines
1.6 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { RedisService } from 'nestjs-redis';
|
|
import * as uuid from 'uuid';
|
|
import { ApplicationException } from '../exceptions/application.exception';
|
|
|
|
export interface RedisMutexOption {
|
|
/**
|
|
* seconds
|
|
*/
|
|
expires?: number;
|
|
/**
|
|
* seconds
|
|
*/
|
|
timeout?: number | null;
|
|
/**
|
|
* milliseconds
|
|
*/
|
|
retryDelay?: number;
|
|
}
|
|
|
|
@Injectable()
|
|
export class RedisMutexService {
|
|
constructor(private readonly redisClient: RedisService) {}
|
|
|
|
public async lock(
|
|
key: string,
|
|
{ expires = 100, timeout = 10, retryDelay = 100 }: RedisMutexOption = {
|
|
expires: 100,
|
|
timeout: 10,
|
|
retryDelay: 100,
|
|
},
|
|
) {
|
|
const redisKey = `${'mutex-lock'}:${key}`;
|
|
const redis = this.redisClient.getClient();
|
|
const value = uuid.v4();
|
|
const timeoutAt = timeout ? Date.now() + timeout * 1000 : null;
|
|
|
|
while (
|
|
!(await redis
|
|
.set(redisKey, value, 'EX', expires, 'NX')
|
|
.then(() => true)
|
|
.catch(() => false))
|
|
) {
|
|
if (timeoutAt && timeoutAt > Date.now()) {
|
|
throw new ApplicationException('lock timeout');
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
}
|
|
|
|
const renewTimer = setInterval(() => {
|
|
redis.expire(redisKey, expires);
|
|
}, (expires * 1000) / 2);
|
|
|
|
return async () => {
|
|
clearInterval(renewTimer);
|
|
await redis.eval(
|
|
`
|
|
if redis.call("get", KEYS[1]) == ARGV[1]
|
|
then
|
|
return redis.call("del", KEYS[1])
|
|
else
|
|
return 0
|
|
end
|
|
`,
|
|
1,
|
|
redisKey,
|
|
value,
|
|
);
|
|
};
|
|
}
|
|
}
|