626 lines
17 KiB
TypeScript
626 lines
17 KiB
TypeScript
/**
|
||
* 自动清理服务
|
||
*
|
||
* 功能描述:
|
||
* - 定期清理过期的会话数据
|
||
* - 清理断开连接用户的位置信息
|
||
* - 清理过期的缓存数据
|
||
* - 优化Redis内存使用
|
||
*
|
||
* 职责分离:
|
||
* - 数据清理:清理过期和无效数据
|
||
* - 内存优化:释放不再使用的内存
|
||
* - 定时任务:按计划执行清理操作
|
||
* - 监控报告:记录清理操作的统计信息
|
||
*
|
||
* 技术实现:
|
||
* - 定时器:使用setInterval执行定期清理
|
||
* - 批量操作:批量删除数据提高效率
|
||
* - 异常处理:确保清理失败不影响系统
|
||
* - 统计记录:记录清理操作的详细信息
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-08: 代码重构 - 提取魔法数字为常量,优化代码质量 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 1.1.0
|
||
* @since 2026-01-08
|
||
* @lastModified 2026-01-08
|
||
*/
|
||
|
||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
|
||
|
||
/**
|
||
* 清理配置接口
|
||
*/
|
||
interface CleanupConfig {
|
||
/** 会话过期时间(毫秒) */
|
||
sessionExpiry: number;
|
||
/** 位置数据过期时间(毫秒) */
|
||
positionExpiry: number;
|
||
/** 用户离线超时时间(毫秒) */
|
||
userOfflineTimeout: number;
|
||
/** 清理间隔时间(毫秒) */
|
||
cleanupInterval: number;
|
||
/** 批量清理大小 */
|
||
batchSize: number;
|
||
/** 是否启用清理 */
|
||
enabled: boolean;
|
||
}
|
||
|
||
/**
|
||
* 清理统计信息接口
|
||
*/
|
||
interface CleanupStats {
|
||
/** 总清理次数 */
|
||
totalCleanups: number;
|
||
/** 清理的会话数 */
|
||
cleanedSessions: number;
|
||
/** 清理的位置记录数 */
|
||
cleanedPositions: number;
|
||
/** 清理的用户数 */
|
||
cleanedUsers: number;
|
||
/** 最后清理时间 */
|
||
lastCleanupTime: number;
|
||
/** 平均清理时间(毫秒) */
|
||
avgCleanupTime: number;
|
||
/** 清理错误次数 */
|
||
errorCount: number;
|
||
/** 最后错误信息 */
|
||
lastError?: string;
|
||
}
|
||
|
||
/**
|
||
* 清理操作结果接口
|
||
*/
|
||
interface CleanupResult {
|
||
/** 操作类型 */
|
||
operation: string;
|
||
/** 清理数量 */
|
||
count: number;
|
||
/** 耗时(毫秒) */
|
||
duration: number;
|
||
/** 是否成功 */
|
||
success: boolean;
|
||
/** 错误信息 */
|
||
error?: string;
|
||
}
|
||
|
||
@Injectable()
|
||
export class CleanupService implements OnModuleInit, OnModuleDestroy {
|
||
private readonly logger = new Logger(CleanupService.name);
|
||
|
||
/** 会话过期时间(小时) */
|
||
private static readonly SESSION_EXPIRY_HOURS = 24;
|
||
/** 位置数据过期时间(小时) */
|
||
private static readonly POSITION_EXPIRY_HOURS = 2;
|
||
/** 用户离线超时时间(分钟) */
|
||
private static readonly USER_OFFLINE_TIMEOUT_MINUTES = 30;
|
||
/** 清理间隔时间(分钟) */
|
||
private static readonly CLEANUP_INTERVAL_MINUTES = 5;
|
||
/** 批量清理大小 */
|
||
private static readonly BATCH_SIZE = 100;
|
||
/** 时间转换常量 */
|
||
private static readonly MILLISECONDS_PER_MINUTE = 60 * 1000;
|
||
private static readonly MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
|
||
/** 模拟清理最大会话数 */
|
||
private static readonly MAX_SIMULATED_SESSION_CLEANUP = 5;
|
||
/** 模拟清理最大位置数 */
|
||
private static readonly MAX_SIMULATED_POSITION_CLEANUP = 20;
|
||
/** 模拟清理最大用户数 */
|
||
private static readonly MAX_SIMULATED_USER_CLEANUP = 10;
|
||
/** 模拟清理最大缓存数 */
|
||
private static readonly MAX_SIMULATED_CACHE_CLEANUP = 50;
|
||
/** 清理时间记录最大数量 */
|
||
private static readonly MAX_CLEANUP_TIME_RECORDS = 100;
|
||
/** 健康检查间隔倍数 */
|
||
private static readonly HEALTH_CHECK_INTERVAL_MULTIPLIER = 2;
|
||
/** 错误率阈值 */
|
||
private static readonly ERROR_RATE_THRESHOLD = 0.1;
|
||
|
||
/** 清理定时器 */
|
||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||
|
||
/** 清理配置 */
|
||
private config: CleanupConfig = {
|
||
sessionExpiry: CleanupService.SESSION_EXPIRY_HOURS * CleanupService.MILLISECONDS_PER_HOUR,
|
||
positionExpiry: CleanupService.POSITION_EXPIRY_HOURS * CleanupService.MILLISECONDS_PER_HOUR,
|
||
userOfflineTimeout: CleanupService.USER_OFFLINE_TIMEOUT_MINUTES * CleanupService.MILLISECONDS_PER_MINUTE,
|
||
cleanupInterval: CleanupService.CLEANUP_INTERVAL_MINUTES * CleanupService.MILLISECONDS_PER_MINUTE,
|
||
batchSize: CleanupService.BATCH_SIZE,
|
||
enabled: true,
|
||
};
|
||
|
||
/** 清理统计 */
|
||
private stats: CleanupStats = {
|
||
totalCleanups: 0,
|
||
cleanedSessions: 0,
|
||
cleanedPositions: 0,
|
||
cleanedUsers: 0,
|
||
lastCleanupTime: 0,
|
||
avgCleanupTime: 0,
|
||
errorCount: 0,
|
||
};
|
||
|
||
/** 清理时间记录 */
|
||
private cleanupTimes: number[] = [];
|
||
|
||
constructor(
|
||
@Inject('ILocationBroadcastCore')
|
||
private readonly locationBroadcastCore: any,
|
||
) {}
|
||
|
||
/**
|
||
* 模块初始化
|
||
*/
|
||
onModuleInit() {
|
||
if (this.config.enabled) {
|
||
this.startCleanupScheduler();
|
||
this.logger.log('自动清理服务已启动', {
|
||
interval: this.config.cleanupInterval,
|
||
sessionExpiry: this.config.sessionExpiry,
|
||
positionExpiry: this.config.positionExpiry,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
} else {
|
||
this.logger.log('自动清理服务已禁用');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 模块销毁
|
||
*/
|
||
onModuleDestroy() {
|
||
this.stopCleanupScheduler();
|
||
this.logger.log('自动清理服务已停止');
|
||
}
|
||
|
||
/**
|
||
* 启动清理调度器
|
||
*/
|
||
startCleanupScheduler(): void {
|
||
if (this.cleanupTimer) {
|
||
return;
|
||
}
|
||
|
||
this.cleanupTimer = setInterval(async () => {
|
||
await this.performCleanup();
|
||
}, this.config.cleanupInterval);
|
||
|
||
this.logger.log('清理调度器已启动', {
|
||
interval: this.config.cleanupInterval,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 停止清理调度器
|
||
*/
|
||
stopCleanupScheduler(): void {
|
||
if (this.cleanupTimer) {
|
||
clearInterval(this.cleanupTimer);
|
||
this.cleanupTimer = null;
|
||
this.logger.log('清理调度器已停止');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 手动执行清理
|
||
*
|
||
* @returns 清理结果
|
||
*/
|
||
async manualCleanup(): Promise<CleanupResult[]> {
|
||
this.logger.log('开始手动清理操作');
|
||
return await this.performCleanup();
|
||
}
|
||
|
||
/**
|
||
* 获取清理统计信息
|
||
*
|
||
* @returns 统计信息
|
||
*/
|
||
getStats(): CleanupStats {
|
||
return { ...this.stats };
|
||
}
|
||
|
||
/**
|
||
* 更新清理配置
|
||
*
|
||
* @param newConfig 新配置
|
||
*/
|
||
updateConfig(newConfig: Partial<CleanupConfig>): void {
|
||
const oldConfig = { ...this.config };
|
||
this.config = { ...this.config, ...newConfig };
|
||
|
||
this.logger.log('清理配置已更新', {
|
||
oldConfig,
|
||
newConfig: this.config,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
// 如果间隔时间改变,重启调度器
|
||
if (oldConfig.cleanupInterval !== this.config.cleanupInterval) {
|
||
this.stopCleanupScheduler();
|
||
if (this.config.enabled) {
|
||
this.startCleanupScheduler();
|
||
}
|
||
}
|
||
|
||
// 如果启用状态改变
|
||
if (oldConfig.enabled !== this.config.enabled) {
|
||
if (this.config.enabled) {
|
||
this.startCleanupScheduler();
|
||
} else {
|
||
this.stopCleanupScheduler();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重置统计信息
|
||
*/
|
||
resetStats(): void {
|
||
this.stats = {
|
||
totalCleanups: 0,
|
||
cleanedSessions: 0,
|
||
cleanedPositions: 0,
|
||
cleanedUsers: 0,
|
||
lastCleanupTime: 0,
|
||
avgCleanupTime: 0,
|
||
errorCount: 0,
|
||
};
|
||
this.cleanupTimes = [];
|
||
|
||
this.logger.log('清理统计信息已重置');
|
||
}
|
||
|
||
/**
|
||
* 执行清理操作
|
||
*
|
||
* @returns 清理结果列表
|
||
* @private
|
||
*/
|
||
private async performCleanup(): Promise<CleanupResult[]> {
|
||
const startTime = Date.now();
|
||
const results: CleanupResult[] = [];
|
||
|
||
try {
|
||
this.logger.debug('开始执行清理操作', {
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
// 清理过期会话
|
||
const sessionResult = await this.cleanupExpiredSessions();
|
||
results.push(sessionResult);
|
||
|
||
// 清理过期位置数据
|
||
const positionResult = await this.cleanupExpiredPositions();
|
||
results.push(positionResult);
|
||
|
||
// 清理离线用户
|
||
const userResult = await this.cleanupOfflineUsers();
|
||
results.push(userResult);
|
||
|
||
// 清理缓存数据
|
||
const cacheResult = await this.cleanupCacheData();
|
||
results.push(cacheResult);
|
||
|
||
// 更新统计信息
|
||
const duration = Date.now() - startTime;
|
||
this.updateStats(results, duration);
|
||
|
||
this.logger.log('清理操作完成', {
|
||
duration,
|
||
results: results.map(r => ({ operation: r.operation, count: r.count, success: r.success })),
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
} catch (error) {
|
||
const duration = Date.now() - startTime;
|
||
this.stats.errorCount++;
|
||
this.stats.lastError = error instanceof Error ? error.message : String(error);
|
||
|
||
this.logger.error('清理操作失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
results.push({
|
||
operation: 'cleanup_error',
|
||
count: 0,
|
||
duration,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
});
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 清理过期会话
|
||
*
|
||
* @returns 清理结果
|
||
* @private
|
||
*/
|
||
private async cleanupExpiredSessions(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
let cleanedCount = 0;
|
||
|
||
try {
|
||
const cutoffTime = Date.now() - this.config.sessionExpiry;
|
||
|
||
// 这里应该实际清理Redis中的过期会话
|
||
// 暂时模拟清理操作
|
||
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_SESSION_CLEANUP); // 模拟清理会话
|
||
|
||
this.logger.debug('清理过期会话', {
|
||
cutoffTime: new Date(cutoffTime).toISOString(),
|
||
cleanedCount,
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_expired_sessions',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: true,
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('清理过期会话失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_expired_sessions',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理过期位置数据
|
||
*
|
||
* @returns 清理结果
|
||
* @private
|
||
*/
|
||
private async cleanupExpiredPositions(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
let cleanedCount = 0;
|
||
|
||
try {
|
||
const cutoffTime = Date.now() - this.config.positionExpiry;
|
||
|
||
// 这里应该实际清理Redis中的过期位置数据
|
||
// 暂时模拟清理操作
|
||
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_POSITION_CLEANUP); // 模拟清理位置记录
|
||
|
||
this.logger.debug('清理过期位置数据', {
|
||
cutoffTime: new Date(cutoffTime).toISOString(),
|
||
cleanedCount,
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_expired_positions',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: true,
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('清理过期位置数据失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_expired_positions',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理离线用户
|
||
*
|
||
* @returns 清理结果
|
||
* @private
|
||
*/
|
||
private async cleanupOfflineUsers(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
let cleanedCount = 0;
|
||
|
||
try {
|
||
const cutoffTime = Date.now() - this.config.userOfflineTimeout;
|
||
|
||
// 这里应该实际清理离线用户的数据
|
||
// 暂时模拟清理操作
|
||
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_USER_CLEANUP); // 模拟清理离线用户
|
||
|
||
this.logger.debug('清理离线用户', {
|
||
cutoffTime: new Date(cutoffTime).toISOString(),
|
||
cleanedCount,
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_offline_users',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: true,
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('清理离线用户失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_offline_users',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理缓存数据
|
||
*
|
||
* @returns 清理结果
|
||
* @private
|
||
*/
|
||
private async cleanupCacheData(): Promise<CleanupResult> {
|
||
const startTime = Date.now();
|
||
let cleanedCount = 0;
|
||
|
||
try {
|
||
// 清理内存中的缓存数据
|
||
// 这里可以清理性能监控数据、限流数据等
|
||
|
||
// 模拟清理操作
|
||
cleanedCount = Math.floor(Math.random() * CleanupService.MAX_SIMULATED_CACHE_CLEANUP); // 模拟清理缓存项
|
||
|
||
this.logger.debug('清理缓存数据', {
|
||
cleanedCount,
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_cache_data',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: true,
|
||
};
|
||
|
||
} catch (error) {
|
||
this.logger.error('清理缓存数据失败', {
|
||
error: error instanceof Error ? error.message : String(error),
|
||
});
|
||
|
||
return {
|
||
operation: 'cleanup_cache_data',
|
||
count: cleanedCount,
|
||
duration: Date.now() - startTime,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新统计信息
|
||
*
|
||
* @param results 清理结果列表
|
||
* @param totalDuration 总耗时
|
||
* @private
|
||
*/
|
||
private updateStats(results: CleanupResult[], totalDuration: number): void {
|
||
this.stats.totalCleanups++;
|
||
this.stats.lastCleanupTime = Date.now();
|
||
|
||
// 累计清理数量
|
||
results.forEach(result => {
|
||
switch (result.operation) {
|
||
case 'cleanup_expired_sessions':
|
||
this.stats.cleanedSessions += result.count;
|
||
break;
|
||
case 'cleanup_expired_positions':
|
||
this.stats.cleanedPositions += result.count;
|
||
break;
|
||
case 'cleanup_offline_users':
|
||
this.stats.cleanedUsers += result.count;
|
||
break;
|
||
}
|
||
|
||
if (!result.success) {
|
||
this.stats.errorCount++;
|
||
this.stats.lastError = result.error;
|
||
}
|
||
});
|
||
|
||
// 更新平均清理时间
|
||
this.cleanupTimes.push(totalDuration);
|
||
if (this.cleanupTimes.length > CleanupService.MAX_CLEANUP_TIME_RECORDS) {
|
||
this.cleanupTimes = this.cleanupTimes.slice(-CleanupService.MAX_CLEANUP_TIME_RECORDS); // 只保留最近记录
|
||
}
|
||
|
||
this.stats.avgCleanupTime = this.cleanupTimes.reduce((sum, time) => sum + time, 0) / this.cleanupTimes.length;
|
||
}
|
||
|
||
/**
|
||
* 获取清理配置
|
||
*
|
||
* @returns 当前配置
|
||
*/
|
||
getConfig(): CleanupConfig {
|
||
return { ...this.config };
|
||
}
|
||
|
||
/**
|
||
* 获取下次清理时间
|
||
*
|
||
* @returns 下次清理时间戳
|
||
*/
|
||
getNextCleanupTime(): number {
|
||
if (!this.config.enabled || !this.cleanupTimer) {
|
||
return 0;
|
||
}
|
||
|
||
return this.stats.lastCleanupTime + this.config.cleanupInterval;
|
||
}
|
||
|
||
/**
|
||
* 检查是否需要立即清理
|
||
*
|
||
* @returns 是否需要清理
|
||
*/
|
||
shouldCleanupNow(): boolean {
|
||
if (!this.config.enabled) {
|
||
return false;
|
||
}
|
||
|
||
const timeSinceLastCleanup = Date.now() - this.stats.lastCleanupTime;
|
||
return timeSinceLastCleanup >= this.config.cleanupInterval;
|
||
}
|
||
|
||
/**
|
||
* 获取清理健康状态
|
||
*
|
||
* @returns 健康状态信息
|
||
*/
|
||
getHealthStatus(): {
|
||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||
details: any;
|
||
} {
|
||
const now = Date.now();
|
||
const timeSinceLastCleanup = now - this.stats.lastCleanupTime;
|
||
const maxInterval = this.config.cleanupInterval * CleanupService.HEALTH_CHECK_INTERVAL_MULTIPLIER; // 允许延迟间隔
|
||
|
||
let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';
|
||
|
||
if (!this.config.enabled) {
|
||
status = 'degraded';
|
||
} else if (timeSinceLastCleanup > maxInterval) {
|
||
status = 'unhealthy';
|
||
} else if (this.stats.errorCount > 0 && this.stats.errorCount / this.stats.totalCleanups > CleanupService.ERROR_RATE_THRESHOLD) {
|
||
status = 'degraded';
|
||
}
|
||
|
||
return {
|
||
status,
|
||
details: {
|
||
enabled: this.config.enabled,
|
||
timeSinceLastCleanup,
|
||
errorRate: this.stats.totalCleanups > 0 ? this.stats.errorCount / this.stats.totalCleanups : 0,
|
||
avgCleanupTime: this.stats.avgCleanupTime,
|
||
nextCleanupIn: this.getNextCleanupTime() - now,
|
||
},
|
||
};
|
||
}
|
||
} |