Files
nxxmdata/backend/services/cacheService.js
2025-09-12 20:08:42 +08:00

744 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Redis缓存服务
* @file cacheService.js
* @description 提供高性能Redis缓存功能优化数据访问速度
*/
const redis = require('redis');
const logger = require('../utils/logger');
/**
* 缓存配置
*/
const CACHE_CONFIG = {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || '',
db: process.env.REDIS_DB || 0,
// 默认TTL配置
ttl: {
short: 5 * 60, // 5分钟
medium: 30 * 60, // 30分钟
long: 2 * 60 * 60, // 2小时
daily: 24 * 60 * 60 // 24小时
},
// 键名前缀
prefix: 'nxxm:',
// 连接配置
connect_timeout: 10000,
command_timeout: 5000,
retry_unfulfilled_commands: true,
retry_delay_on_failure: 100,
max_retry_delay: 3000
};
/**
* Redis缓存服务类
*/
class CacheService {
constructor() {
this.client = null;
this.isConnected = false;
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalOperations: 0
};
}
/**
* 初始化Redis连接
*/
async init() {
try {
this.client = redis.createClient({
url: `redis://${CACHE_CONFIG.host}:${CACHE_CONFIG.port}`,
password: CACHE_CONFIG.password || undefined,
database: CACHE_CONFIG.db,
socket: {
connectTimeout: CACHE_CONFIG.connect_timeout,
commandTimeout: CACHE_CONFIG.command_timeout,
reconnectStrategy: (retries) => {
if (retries > 10) {
logger.error('Redis重连次数超过限制停止重连');
return false;
}
return Math.min(retries * 100, 3000);
}
}
});
// 事件监听
this.client.on('connect', () => {
logger.info('Redis连接已建立');
this.isConnected = true;
});
this.client.on('ready', () => {
logger.info('Redis客户端已就绪');
});
this.client.on('error', (error) => {
logger.error('Redis连接错误:', error);
this.isConnected = false;
this.stats.errors++;
});
this.client.on('end', () => {
logger.warn('Redis连接已断开');
this.isConnected = false;
});
this.client.on('reconnecting', () => {
logger.info('正在重新连接Redis...');
});
// 连接到Redis
await this.client.connect();
logger.info('Redis缓存服务初始化成功');
} catch (error) {
logger.error('Redis缓存服务初始化失败:', error);
this.isConnected = false;
throw error;
}
}
/**
* 生成缓存键名
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {string} 完整键名
*/
generateKey(key, namespace = 'default') {
return `${CACHE_CONFIG.prefix}${namespace}:${key}`;
}
/**
* 设置缓存
* @param {string} key 键名
* @param {*} value 值
* @param {number} ttl 过期时间(秒)
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async set(key, value, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
if (!this.isConnected) {
logger.warn('Redis未连接缓存设置跳过');
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const serializedValue = JSON.stringify(value);
await this.client.setEx(fullKey, ttl, serializedValue);
this.stats.totalOperations++;
logger.debug(`缓存设置成功: ${fullKey}, TTL: ${ttl}s`);
return true;
} catch (error) {
logger.error(`缓存设置失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 获取缓存
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {*} 缓存值或null
*/
async get(key, namespace = 'default') {
if (!this.isConnected) {
logger.warn('Redis未连接缓存获取跳过');
return null;
}
try {
const fullKey = this.generateKey(key, namespace);
const value = await this.client.get(fullKey);
this.stats.totalOperations++;
if (value === null) {
this.stats.misses++;
logger.debug(`缓存未命中: ${fullKey}`);
return null;
}
this.stats.hits++;
logger.debug(`缓存命中: ${fullKey}`);
return JSON.parse(value);
} catch (error) {
logger.error(`缓存获取失败 ${key}:`, error);
this.stats.errors++;
return null;
}
}
/**
* 删除缓存
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {boolean} 删除结果
*/
async del(key, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.del(fullKey);
this.stats.totalOperations++;
logger.debug(`缓存删除: ${fullKey}, 结果: ${result}`);
return result > 0;
} catch (error) {
logger.error(`缓存删除失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 检查缓存是否存在
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {boolean} 是否存在
*/
async exists(key, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.exists(fullKey);
this.stats.totalOperations++;
return result === 1;
} catch (error) {
logger.error(`缓存存在检查失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 设置过期时间
* @param {string} key 键名
* @param {number} ttl 过期时间(秒)
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async expire(key, ttl, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.expire(fullKey, ttl);
this.stats.totalOperations++;
return result === 1;
} catch (error) {
logger.error(`设置过期时间失败 ${key}:`, error);
this.stats.errors++;
return false;
}
}
/**
* 获取剩余过期时间
* @param {string} key 键名
* @param {string} namespace 命名空间
* @returns {number} 剩余秒数,-1表示永不过期-2表示键不存在
*/
async ttl(key, namespace = 'default') {
if (!this.isConnected) {
return -2;
}
try {
const fullKey = this.generateKey(key, namespace);
const result = await this.client.ttl(fullKey);
this.stats.totalOperations++;
return result;
} catch (error) {
logger.error(`获取过期时间失败 ${key}:`, error);
this.stats.errors++;
return -2;
}
}
/**
* 清空指定命名空间的所有缓存
* @param {string} namespace 命名空间
* @returns {number} 删除的键数量
*/
async clearNamespace(namespace) {
if (!this.isConnected) {
return 0;
}
try {
const pattern = `${CACHE_CONFIG.prefix}${namespace}:*`;
const keys = await this.client.keys(pattern);
if (keys.length === 0) {
return 0;
}
const result = await this.client.del(keys);
this.stats.totalOperations++;
logger.info(`清空命名空间 ${namespace}: 删除了 ${result} 个键`);
return result;
} catch (error) {
logger.error(`清空命名空间失败 ${namespace}:`, error);
this.stats.errors++;
return 0;
}
}
/**
* 批量设置缓存
* @param {Object} keyValuePairs 键值对
* @param {number} ttl 过期时间
* @param {string} namespace 命名空间
* @returns {boolean} 设置结果
*/
async mSet(keyValuePairs, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
if (!this.isConnected) {
return false;
}
try {
const pipeline = this.client.multi();
Object.entries(keyValuePairs).forEach(([key, value]) => {
const fullKey = this.generateKey(key, namespace);
const serializedValue = JSON.stringify(value);
pipeline.setEx(fullKey, ttl, serializedValue);
});
await pipeline.exec();
this.stats.totalOperations += Object.keys(keyValuePairs).length;
logger.debug(`批量缓存设置成功: ${Object.keys(keyValuePairs).length} 个键`);
return true;
} catch (error) {
logger.error('批量缓存设置失败:', error);
this.stats.errors++;
return false;
}
}
/**
* 批量获取缓存
* @param {Array} keys 键名数组
* @param {string} namespace 命名空间
* @returns {Object} 键值对结果
*/
async mGet(keys, namespace = 'default') {
if (!this.isConnected) {
return {};
}
try {
const fullKeys = keys.map(key => this.generateKey(key, namespace));
const values = await this.client.mGet(fullKeys);
const result = {};
keys.forEach((key, index) => {
const value = values[index];
if (value !== null) {
try {
result[key] = JSON.parse(value);
this.stats.hits++;
} catch (error) {
logger.warn(`解析缓存值失败 ${key}:`, error);
result[key] = null;
}
} else {
result[key] = null;
this.stats.misses++;
}
});
this.stats.totalOperations += keys.length;
return result;
} catch (error) {
logger.error('批量缓存获取失败:', error);
this.stats.errors++;
return {};
}
}
/**
* 缓存包装器 - 自动缓存函数结果
* @param {string} key 缓存键
* @param {Function} fn 异步函数
* @param {number} ttl 过期时间
* @param {string} namespace 命名空间
* @returns {*} 函数结果
*/
async wrap(key, fn, ttl = CACHE_CONFIG.ttl.medium, namespace = 'default') {
// 首先尝试从缓存获取
const cached = await this.get(key, namespace);
if (cached !== null) {
return cached;
}
try {
// 缓存未命中,执行函数
const result = await fn();
// 将结果存入缓存
await this.set(key, result, ttl, namespace);
return result;
} catch (error) {
logger.error(`缓存包装器执行失败 ${key}:`, error);
throw error;
}
}
/**
* 获取缓存统计信息
* @returns {Object} 统计信息
*/
getStats() {
const hitRate = this.stats.totalOperations > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses)) * 100
: 0;
return {
...this.stats,
hitRate: hitRate.toFixed(2) + '%',
isConnected: this.isConnected,
config: {
host: CACHE_CONFIG.host,
port: CACHE_CONFIG.port,
db: CACHE_CONFIG.db
}
};
}
/**
* 获取Redis服务器信息
* @returns {Object} 服务器信息
*/
async getServerInfo() {
if (!this.isConnected) {
return null;
}
try {
const info = await this.client.info();
const memory = await this.client.info('memory');
const stats = await this.client.info('stats');
return {
version: this.extractInfoValue(info, 'redis_version'),
uptime: this.extractInfoValue(info, 'uptime_in_seconds'),
memory: {
used: this.extractInfoValue(memory, 'used_memory_human'),
peak: this.extractInfoValue(memory, 'used_memory_peak_human'),
fragmentation: this.extractInfoValue(memory, 'mem_fragmentation_ratio')
},
stats: {
connections: this.extractInfoValue(stats, 'total_connections_received'),
commands: this.extractInfoValue(stats, 'total_commands_processed'),
hits: this.extractInfoValue(stats, 'keyspace_hits'),
misses: this.extractInfoValue(stats, 'keyspace_misses')
}
};
} catch (error) {
logger.error('获取Redis服务器信息失败:', error);
return null;
}
}
/**
* 从INFO字符串中提取值
* @private
*/
extractInfoValue(infoString, key) {
const regex = new RegExp(`${key}:(.+)`);
const match = infoString.match(regex);
return match ? match[1].trim() : '';
}
/**
* 健康检查
* @returns {Object} 健康状态
*/
async healthCheck() {
try {
if (!this.isConnected) {
return {
status: 'unhealthy',
message: 'Redis连接断开',
timestamp: new Date()
};
}
// 执行简单的ping测试
const pong = await this.client.ping();
if (pong === 'PONG') {
return {
status: 'healthy',
message: 'Redis连接正常',
stats: this.getStats(),
timestamp: new Date()
};
} else {
return {
status: 'unhealthy',
message: 'Redis ping响应异常',
timestamp: new Date()
};
}
} catch (error) {
logger.error('Redis健康检查失败:', error);
return {
status: 'unhealthy',
message: 'Redis健康检查失败',
error: error.message,
timestamp: new Date()
};
}
}
/**
* 关闭连接
*/
async close() {
try {
if (this.client && this.isConnected) {
await this.client.quit();
logger.info('Redis连接已关闭');
}
} catch (error) {
logger.error('关闭Redis连接失败:', error);
}
}
/**
* 清空所有缓存
* @returns {boolean} 清空结果
*/
async flushAll() {
if (!this.isConnected) {
return false;
}
try {
await this.client.flushDb();
logger.info('Redis缓存已清空');
// 重置统计
this.stats = {
hits: 0,
misses: 0,
errors: 0,
totalOperations: 0
};
return true;
} catch (error) {
logger.error('清空Redis缓存失败:', error);
this.stats.errors++;
return false;
}
}
}
/**
* 缓存键名常量
*/
const CACHE_KEYS = {
// 用户相关
USER_LIST: 'users:list',
USER_PROFILE: (id) => `users:profile:${id}`,
USER_PERMISSIONS: (id) => `users:permissions:${id}`,
// 农场相关
FARM_LIST: 'farms:list',
FARM_DETAIL: (id) => `farms:detail:${id}`,
FARM_ANIMALS: (id) => `farms:animals:${id}`,
FARM_DEVICES: (id) => `farms:devices:${id}`,
// 设备相关
DEVICE_LIST: 'devices:list',
DEVICE_STATUS: (id) => `devices:status:${id}`,
DEVICE_METRICS: (id) => `devices:metrics:${id}`,
// 统计数据
STATS_DASHBOARD: 'stats:dashboard',
STATS_FARMS: 'stats:farms',
STATS_DEVICES: 'stats:devices',
STATS_ANIMALS: 'stats:animals',
STATS_ALERTS: 'stats:alerts',
// 系统配置
SYSTEM_CONFIG: 'system:config',
MENU_PERMISSIONS: 'system:menus',
// 搜索结果
SEARCH_RESULTS: (type, query) => `search:${type}:${Buffer.from(query).toString('base64')}`
};
/**
* 高级缓存功能
*/
class AdvancedCache extends CacheService {
/**
* 缓存列表数据(带分页)
* @param {string} key 基础键名
* @param {Array} data 数据数组
* @param {Object} pagination 分页信息
* @param {number} ttl 过期时间
*/
async setListData(key, data, pagination = {}, ttl = CACHE_CONFIG.ttl.medium) {
const cacheData = {
data,
pagination,
timestamp: Date.now()
};
return await this.set(key, cacheData, ttl, 'lists');
}
/**
* 获取列表数据
* @param {string} key 键名
* @returns {Object} 列表数据和分页信息
*/
async getListData(key) {
const cached = await this.get(key, 'lists');
if (cached && cached.data) {
return {
data: cached.data,
pagination: cached.pagination || {},
fromCache: true,
cacheTimestamp: cached.timestamp
};
}
return null;
}
/**
* 智能缓存失效
* @param {string} entity 实体类型
* @param {string} operation 操作类型
* @param {number} entityId 实体ID
*/
async invalidateRelated(entity, operation, entityId = null) {
const patterns = [];
switch (entity) {
case 'farm':
patterns.push('farms:*', 'stats:*');
if (entityId) {
patterns.push(`farms:detail:${entityId}`, `farms:animals:${entityId}`, `farms:devices:${entityId}`);
}
break;
case 'device':
patterns.push('devices:*', 'stats:*');
if (entityId) {
patterns.push(`devices:status:${entityId}`, `devices:metrics:${entityId}`);
}
break;
case 'animal':
patterns.push('animals:*', 'stats:*');
break;
case 'user':
patterns.push('users:*');
if (entityId) {
patterns.push(`users:profile:${entityId}`, `users:permissions:${entityId}`);
}
break;
default:
patterns.push('stats:*'); // 默认清理统计缓存
}
let totalDeleted = 0;
for (const pattern of patterns) {
const deleted = await this.clearPattern(pattern);
totalDeleted += deleted;
}
logger.info(`智能缓存失效: ${entity}/${operation}, 清理了 ${totalDeleted} 个缓存键`);
return totalDeleted;
}
/**
* 按模式清理缓存
* @private
*/
async clearPattern(pattern) {
if (!this.isConnected) {
return 0;
}
try {
const fullPattern = `${CACHE_CONFIG.prefix}${pattern}`;
const keys = await this.client.keys(fullPattern);
if (keys.length === 0) {
return 0;
}
const result = await this.client.del(keys);
return result;
} catch (error) {
logger.error(`按模式清理缓存失败 ${pattern}:`, error);
return 0;
}
}
}
// 创建缓存服务实例
const cacheService = new AdvancedCache();
// 优雅关闭处理
process.on('SIGINT', async () => {
logger.info('收到SIGINT信号正在关闭Redis连接...');
await cacheService.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('收到SIGTERM信号正在关闭Redis连接...');
await cacheService.close();
process.exit(0);
});
module.exports = {
cacheService,
CACHE_KEYS,
CACHE_CONFIG
};