167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||
import { ConfigService } from '@nestjs/config';
|
||
import { createClient, RedisClientType } from 'redis';
|
||
import { CustomLogger } from '@/common/logger/logger.service';
|
||
|
||
// Redis 配置接口
|
||
interface RedisConfig {
|
||
connectName: string;
|
||
username: string;
|
||
password: string;
|
||
database: number;
|
||
host: string;
|
||
port: number;
|
||
ttl: number;
|
||
}
|
||
|
||
@Injectable()
|
||
export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||
public client: RedisClientType;
|
||
|
||
constructor(
|
||
private configService: ConfigService,
|
||
private logger: CustomLogger,
|
||
) {
|
||
const config = this.configService.get<RedisConfig>('redis');
|
||
if (!config) {
|
||
throw new Error('Redis 配置未找到');
|
||
}
|
||
|
||
this.client = createClient({
|
||
name: config.connectName,
|
||
username: config.username,
|
||
password: config.password,
|
||
database: config.database,
|
||
url: `redis://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`,
|
||
});
|
||
|
||
this.client.on('connect', () => {
|
||
const connectName =
|
||
this.configService.get<string>('redis.connectName');
|
||
|
||
void this.client.set('SI HI', connectName || '').then((result) => {
|
||
this.logger.log(result || '', 'InitRedis');
|
||
});
|
||
});
|
||
|
||
this.client.on('error', (err: Error) => {
|
||
this.logger.error(err.message, err.stack, 'InitRedis');
|
||
});
|
||
}
|
||
|
||
async onModuleInit() {
|
||
await this.client.connect();
|
||
}
|
||
|
||
async onModuleDestroy() {
|
||
await this.client.quit();
|
||
}
|
||
|
||
/**
|
||
* 设置键值对
|
||
* @param key 键
|
||
* @param value 值
|
||
* @param ttl 过期时间(秒)
|
||
*/
|
||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||
const expireTime = ttl || this.configService.get('redis.ttl');
|
||
await this.client.set(key, value, { EX: expireTime });
|
||
}
|
||
|
||
/**
|
||
* 获取键值
|
||
* @param key 键
|
||
* @returns 值
|
||
*/
|
||
async get(key: string): Promise<string | null> {
|
||
return await this.client.get(key);
|
||
}
|
||
|
||
/**
|
||
* 删除键
|
||
* @param key 键
|
||
*/
|
||
async del(key: string): Promise<void> {
|
||
await this.client.del(key);
|
||
}
|
||
|
||
/**
|
||
* 检查键是否存在
|
||
* @param key 键
|
||
* @returns 是否存在
|
||
*/
|
||
async exists(key: string): Promise<boolean> {
|
||
return (await this.client.exists(key)) === 1;
|
||
}
|
||
|
||
/**
|
||
* 设置键的过期时间
|
||
* @param key 键
|
||
* @param ttl 过期时间(秒)
|
||
*/
|
||
async expire(key: string, ttl: number): Promise<void> {
|
||
await this.client.expire(key, ttl);
|
||
}
|
||
|
||
/**
|
||
* 尝试获取分布式锁
|
||
* @param key 锁的键
|
||
* @param ttl 锁的过期时间(秒)
|
||
* @returns 是否成功获取锁
|
||
*/
|
||
async lock(key: string, ttl: number = 30): Promise<boolean> {
|
||
const lockKey = `lock:${key}`;
|
||
const lockValue = Date.now().toString();
|
||
|
||
// 使用 SET NX 命令尝试获取锁
|
||
const result = await this.client.set(lockKey, lockValue, {
|
||
NX: true, // 只有当key不存在时才设置
|
||
EX: ttl, // 设置过期时间
|
||
});
|
||
|
||
return result === 'OK';
|
||
}
|
||
|
||
/**
|
||
* 释放分布式锁
|
||
* @param key 锁的键
|
||
*/
|
||
async unlock(key: string): Promise<void> {
|
||
const lockKey = `lock:${key}`;
|
||
await this.client.del(lockKey);
|
||
}
|
||
|
||
/**
|
||
* 获取键的剩余过期时间
|
||
* @param key 键
|
||
* @returns 剩余过期时间(秒),如果键不存在返回-2,如果键没有过期时间返回-1
|
||
*/
|
||
async ttl(key: string): Promise<number> {
|
||
return await this.client.ttl(key);
|
||
}
|
||
|
||
/**
|
||
* 使用自动释放的分布式锁执行操作
|
||
* @param key 锁的键
|
||
* @param fn 需要在锁中执行的函数
|
||
* @param ttl 锁的过期时间(秒)
|
||
* @returns 函数执行的结果
|
||
*/
|
||
async withLock<T>(
|
||
key: string,
|
||
fn: () => Promise<T>,
|
||
ttl: number = 30,
|
||
): Promise<T> {
|
||
const locked = await this.lock(key, ttl);
|
||
if (!locked) {
|
||
throw new Error('无法获取锁');
|
||
}
|
||
|
||
try {
|
||
return await fn();
|
||
} finally {
|
||
await this.unlock(key);
|
||
}
|
||
}
|
||
}
|