290 lines
6.3 KiB
JavaScript
290 lines
6.3 KiB
JavaScript
const Redis = require('ioredis');
|
|
const logger = require('./logger');
|
|
|
|
/**
|
|
* Redis Connection Configuration
|
|
* Supports both single instance and cluster modes
|
|
*/
|
|
|
|
const redisConfig = {
|
|
host: process.env.REDIS_HOST || 'localhost',
|
|
port: parseInt(process.env.REDIS_PORT) || 6379,
|
|
password: process.env.REDIS_PASSWORD || undefined,
|
|
db: parseInt(process.env.REDIS_DB) || 0,
|
|
retryStrategy: (times) => {
|
|
const delay = Math.min(times * 50, 2000);
|
|
return delay;
|
|
},
|
|
maxRetriesPerRequest: 3,
|
|
enableReadyCheck: true,
|
|
enableOfflineQueue: true,
|
|
lazyConnect: false,
|
|
connectTimeout: 10000,
|
|
keepAlive: 30000,
|
|
family: 4, // IPv4
|
|
// Connection pool settings
|
|
minReconnectInterval: 100,
|
|
maxReconnectInterval: 3000
|
|
};
|
|
|
|
// Create Redis client
|
|
let redisClient = null;
|
|
let isConnected = false;
|
|
|
|
try {
|
|
redisClient = new Redis(redisConfig);
|
|
|
|
// Connection events
|
|
redisClient.on('connect', () => {
|
|
logger.info('Redis client connecting...');
|
|
});
|
|
|
|
redisClient.on('ready', () => {
|
|
isConnected = true;
|
|
logger.info('Redis client connected and ready');
|
|
});
|
|
|
|
redisClient.on('error', (err) => {
|
|
isConnected = false;
|
|
logger.error('Redis client error:', err);
|
|
});
|
|
|
|
redisClient.on('close', () => {
|
|
isConnected = false;
|
|
logger.warn('Redis client connection closed');
|
|
});
|
|
|
|
redisClient.on('reconnecting', () => {
|
|
logger.info('Redis client reconnecting...');
|
|
});
|
|
|
|
redisClient.on('end', () => {
|
|
isConnected = false;
|
|
logger.warn('Redis client connection ended');
|
|
});
|
|
|
|
} catch (error) {
|
|
logger.error('Failed to create Redis client:', error);
|
|
}
|
|
|
|
/**
|
|
* Check if Redis is connected
|
|
*/
|
|
const isRedisConnected = () => {
|
|
return isConnected && redisClient && redisClient.status === 'ready';
|
|
};
|
|
|
|
/**
|
|
* Get Redis client
|
|
*/
|
|
const getRedisClient = () => {
|
|
if (!isRedisConnected()) {
|
|
logger.warn('Redis client not connected');
|
|
return null;
|
|
}
|
|
return redisClient;
|
|
};
|
|
|
|
/**
|
|
* Close Redis connection gracefully
|
|
*/
|
|
const closeRedis = async () => {
|
|
if (redisClient) {
|
|
await redisClient.quit();
|
|
logger.info('Redis connection closed');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Cache helper functions
|
|
*/
|
|
|
|
/**
|
|
* Get cached data
|
|
* @param {string} key - Cache key
|
|
* @returns {Promise<any>} - Parsed JSON data or null
|
|
*/
|
|
const getCache = async (key) => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
logger.warn('Redis not connected, cache miss');
|
|
return null;
|
|
}
|
|
|
|
const data = await redisClient.get(key);
|
|
if (!data) return null;
|
|
|
|
logger.debug(`Cache hit: ${key}`);
|
|
return JSON.parse(data);
|
|
} catch (error) {
|
|
logger.error(`Cache get error for key ${key}:`, error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set cached data
|
|
* @param {string} key - Cache key
|
|
* @param {any} value - Data to cache
|
|
* @param {number} ttl - Time to live in seconds (default: 300 = 5 minutes)
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
const setCache = async (key, value, ttl = 300) => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
logger.warn('Redis not connected, skipping cache set');
|
|
return false;
|
|
}
|
|
|
|
const serialized = JSON.stringify(value);
|
|
await redisClient.setex(key, ttl, serialized);
|
|
|
|
logger.debug(`Cache set: ${key} (TTL: ${ttl}s)`);
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`Cache set error for key ${key}:`, error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delete cached data
|
|
* @param {string} key - Cache key or pattern
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
const deleteCache = async (key) => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
return false;
|
|
}
|
|
|
|
// Support pattern deletion (e.g., "user:*")
|
|
if (key.includes('*')) {
|
|
const keys = await redisClient.keys(key);
|
|
if (keys.length > 0) {
|
|
await redisClient.del(...keys);
|
|
logger.debug(`Cache deleted: ${keys.length} keys matching ${key}`);
|
|
}
|
|
} else {
|
|
await redisClient.del(key);
|
|
logger.debug(`Cache deleted: ${key}`);
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(`Cache delete error for key ${key}:`, error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clear all cache
|
|
* @returns {Promise<boolean>} - Success status
|
|
*/
|
|
const clearCache = async () => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
return false;
|
|
}
|
|
|
|
await redisClient.flushdb();
|
|
logger.info('All cache cleared');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Cache clear error:', error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get multiple keys at once
|
|
* @param {string[]} keys - Array of cache keys
|
|
* @returns {Promise<object>} - Object with key-value pairs
|
|
*/
|
|
const getCacheMultiple = async (keys) => {
|
|
try {
|
|
if (!isRedisConnected() || !keys || keys.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
const values = await redisClient.mget(...keys);
|
|
const result = {};
|
|
|
|
keys.forEach((key, index) => {
|
|
if (values[index]) {
|
|
try {
|
|
result[key] = JSON.parse(values[index]);
|
|
} catch (err) {
|
|
result[key] = null;
|
|
}
|
|
} else {
|
|
result[key] = null;
|
|
}
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
logger.error('Cache mget error:', error);
|
|
return {};
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Increment a counter
|
|
* @param {string} key - Cache key
|
|
* @param {number} increment - Amount to increment (default: 1)
|
|
* @param {number} ttl - Time to live in seconds (optional)
|
|
* @returns {Promise<number>} - New value
|
|
*/
|
|
const incrementCache = async (key, increment = 1, ttl = null) => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
return 0;
|
|
}
|
|
|
|
const newValue = await redisClient.incrby(key, increment);
|
|
|
|
if (ttl) {
|
|
await redisClient.expire(key, ttl);
|
|
}
|
|
|
|
return newValue;
|
|
} catch (error) {
|
|
logger.error(`Cache increment error for key ${key}:`, error);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if key exists
|
|
* @param {string} key - Cache key
|
|
* @returns {Promise<boolean>} - Exists status
|
|
*/
|
|
const cacheExists = async (key) => {
|
|
try {
|
|
if (!isRedisConnected()) {
|
|
return false;
|
|
}
|
|
|
|
const exists = await redisClient.exists(key);
|
|
return exists === 1;
|
|
} catch (error) {
|
|
logger.error(`Cache exists error for key ${key}:`, error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
redisClient,
|
|
isRedisConnected,
|
|
getRedisClient,
|
|
closeRedis,
|
|
getCache,
|
|
setCache,
|
|
deleteCache,
|
|
clearCache,
|
|
getCacheMultiple,
|
|
incrementCache,
|
|
cacheExists
|
|
};
|