744 lines
19 KiB
JavaScript
744 lines
19 KiB
JavaScript
/**
|
||
* 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
|
||
};
|