2026-05-09 18:25:04 +08:00
|
|
|
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
|
|
|
|
|
import { ConfigService } from '@nestjs/config';
|
|
|
|
|
import Redis from 'ioredis';
|
|
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class RedisService implements OnModuleInit, OnModuleDestroy {
|
|
|
|
|
private readonly logger = new Logger(RedisService.name);
|
|
|
|
|
private client: Redis;
|
|
|
|
|
private _connected = false;
|
|
|
|
|
|
|
|
|
|
constructor(private configService: ConfigService) {}
|
|
|
|
|
|
|
|
|
|
async onModuleInit() {
|
|
|
|
|
const url = this.configService.get<string>('redis.url');
|
|
|
|
|
if (url) {
|
2026-05-17 00:39:46 +08:00
|
|
|
this.client = new Redis(url, { lazyConnect: true, retryStrategy: () => null });
|
2026-05-09 18:25:04 +08:00
|
|
|
} else {
|
|
|
|
|
this.client = new Redis({
|
|
|
|
|
host: this.configService.get<string>('redis.host', 'localhost'),
|
|
|
|
|
port: this.configService.get<number>('redis.port', 6379),
|
|
|
|
|
password: this.configService.get<string>('redis.password'),
|
|
|
|
|
db: this.configService.get<number>('redis.db', 0),
|
2026-05-17 00:39:46 +08:00
|
|
|
lazyConnect: true,
|
|
|
|
|
retryStrategy: () => null,
|
2026-05-09 18:25:04 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
this.client.on('connect', () => {
|
|
|
|
|
this._connected = true;
|
|
|
|
|
this.logger.log('Redis connected');
|
|
|
|
|
});
|
|
|
|
|
this.client.on('error', (err) => {
|
|
|
|
|
this._connected = false;
|
|
|
|
|
this.logger.warn(`Redis error: ${err.message}`);
|
|
|
|
|
});
|
2026-05-17 23:00:11 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.client.connect();
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
this.logger.warn(`Redis connect failed: ${err.message}`);
|
|
|
|
|
}
|
2026-05-09 18:25:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async onModuleDestroy() {
|
|
|
|
|
await this.client?.quit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isHealthy(): boolean {
|
|
|
|
|
return this._connected && this.client?.status === 'ready';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async get(key: string): Promise<string | null> {
|
|
|
|
|
return this.client.get(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async set(key: string, value: string, ttl?: number): Promise<void> {
|
|
|
|
|
if (ttl) {
|
|
|
|
|
await this.client.set(key, value, 'EX', ttl);
|
|
|
|
|
} else {
|
|
|
|
|
await this.client.set(key, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async del(key: string): Promise<void> {
|
|
|
|
|
await this.client.del(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async exists(key: string): Promise<boolean> {
|
|
|
|
|
return (await this.client.exists(key)) === 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async expire(key: string, ttl: number): Promise<void> {
|
|
|
|
|
await this.client.expire(key, ttl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async ttl(key: string): Promise<number> {
|
|
|
|
|
return this.client.ttl(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async incr(key: string): Promise<number> {
|
|
|
|
|
return this.client.incr(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setNx(key: string, value: string): Promise<boolean> {
|
|
|
|
|
return (await this.client.setnx(key, value)) === 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async lock(key: string, ttlSeconds: number): Promise<string | null> {
|
|
|
|
|
const token = Math.random().toString(36).substring(2);
|
|
|
|
|
const result = await this.client.set(key, token, 'EX', ttlSeconds, 'NX');
|
|
|
|
|
return result === 'OK' ? token : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async unlock(key: string, token: string): Promise<boolean> {
|
|
|
|
|
const script = `
|
|
|
|
|
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
|
|
|
return redis.call('del', KEYS[1])
|
|
|
|
|
else
|
|
|
|
|
return 0
|
|
|
|
|
end
|
|
|
|
|
`;
|
|
|
|
|
const result = await this.client.eval(script, 1, key, token);
|
|
|
|
|
return result === 1;
|
|
|
|
|
}
|
|
|
|
|
}
|