使用 TypeScript 和 ioredis 在 Node.js 中构建高性能缓存管理器

发布:2024-10-28 09:37 阅读:29 点赞:0

为了提升 Node.js 应用程序的性能,本文介绍了一款基于 ioredis 的缓存管理器类。该类注重易用性和性能优化,支持 TypeScript,并致力于简化缓存操作,优化效率,简化操作流程。

一. 引言

缓存在现代 Web 应用程序中扮演着至关重要的角色。它不仅可以减少数据库负载,还能显著提高应用响应速度。为此,我们开发了一个基于 ioredis 的缓存管理类,该类旨在提供简单易用的 API,同时具备良好的性能表现。以下是该缓存管理类的基本介绍和使用示例。

二. 缓存管理类的设计与实现

1. 类的导入与定义

import Redis, { type RedisOptions } from 'ioredis';

interface CacheConfig {
    defaultTTL?: number;
}

export class CacheManager {

2. 类的构造函数

构造函数用于初始化缓存管理器实例,并设定默认的 TTL 时间(Time To Live)。

private static instance: CacheManager;
private static redisClient: Redis;
private currentKey: string | null;
private defaultTTL: number;

private static readonly DEFAULT_TTL = 3600;

private constructor(config?: CacheConfig) {
    const redisConfig: RedisOptions = {
        db2// 选择数据库 2
        retryStrategy(times: number) => {
            const delay = Math.min(times * 502000); // 设置重试策略
            return delay;
        },
        lazyConnecttrue// 延迟连接
        maxRetriesPerRequest3// 每次请求的最大重试次数
        enableReadyChecktrue// 启用准备检查
        autoResubscribetrue// 自动重新订阅
        autoResendUnfulfilledCommandstrue// 自动重发未完成命令
        reconnectOnError(err: Error) => {
            const targetError = 'READONLY'// 当遇到只读错误时尝试重新连接
            return err.message.includes(targetError);
        },
    };

    if (!CacheManager.redisClient) {
        CacheManager.redisClient = new Redis(redisConfig); // 创建 Redis 实例

        CacheManager.redisClient.on('error', (error: Error) => {
            console.error('Redis Client Error:', error); // 错误监听
        });

        CacheManager.redisClient.on('connect', () => {
            console.debug('Redis Client Connected'); // 连接成功监听
        });

        CacheManager.redisClient.on('ready', () => {
            console.debug('Redis Client Ready'); // 准备就绪监听
        });
    }

    this.currentKey = null// 当前键值
    this.defaultTTL = config?.defaultTTL ?? CacheManager.DEFAULT_TTL; // 默认 TTL
}

3. 单例模式的实现

通过单例模式确保在整个应用程序中只存在一个缓存管理器实例。

public static getInstance(config?: CacheConfig): CacheManager {
    if (!CacheManager.instance) {
        CacheManager.instance = new CacheManager(config);
    }
    return CacheManager.instance;
}

4. 设置键值

设置当前操作的键值,并进行有效性验证。

public key(key: string): CacheManager {
    this.validateKey(key);
    this.currentKey = key;
    return this;
}

private validateKey(key: string): void {
    if (key.length > 100throw new Error('Key too long'); // 验证键长度
    if (!/^[\w:-]+$/.test(key)) throw new Error('Invalid key format'); // 验证键格式
}

5. 获取缓存值

获取当前键对应的缓存值。

public async getValue<T>(): Promise<T | null> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }
        const value = await CacheManager.redisClient.get(this.currentKey); // 从 Redis 获取值
        return value ? JSON.parse(value) : null// 解析 JSON 字符串
    } catch (error) {
        console.error('getValue Error:', error); // 错误处理
        return null;
    }
}

6. 批量获取缓存值

批量获取多个键对应的缓存值。

public async getMultiple<T>(keys: string[]): Promise<Record<string, T | null>> {
    try {
        const pipeline = CacheManager.redisClient.pipeline(); // 创建管道
        for (const key of keys) {
            pipeline.get(key); // 向管道中添加 GET 命令
        }

        type PipelineResult = [Error | null, string | null][] | null;
        const results = (await pipeline.exec()) as PipelineResult; // 执行管道
        const output: Record<string, T | null> = {}; // 创建结果对象

        if (!results) {
            return output;
        }

        keys.forEach((key, index) => {
            const result = results[index];
            if (result) {
                const [err, value] = result;
                if (!err && value) {
                    try {
                        output[key] = JSON.parse(value); // 解析 JSON 字符串
                    } catch {
                        output[key] = null// 处理解析错误
                    }
                } else {
                    output[key] = null// 处理未找到情况
                }
            } else {
                output[key] = null// 处理未找到情况
            }
        });

        return output;
    } catch (error) {
        console.error('getMultiple Error:', error); // 错误处理
        return {};
    }
}

7. 设置缓存值

设置当前键对应的缓存值,并可选设置过期时间。

public async setValue<T>(value: T, ttl: number = this.defaultTTL): Promise<boolean> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }
        const stringValue = JSON.stringify(value); // 将值转换为 JSON 字符串

        if (ttl) {
            await CacheManager.redisClient.setex(this.currentKey, ttl, stringValue); // 设置值并设置过期时间
        } else {
            await CacheManager.redisClient.set(this.currentKey, stringValue); // 设置值
        }

        return true// 操作成功返回 true
    } catch (error) {
        console.error('setValue Error:', error); // 错误处理
        return false// 操作失败返回 false
    }
}

8. 批量设置缓存值

批量设置多个键对应的缓存值,并可选设置过期时间。

public async setBulkValue<T>(keyValuePairs: Record<string, T>, ttl: number = this.defaultTTL, batchSize = 1000): Promise<boolean> {
    try {
        const entries = Object.entries(keyValuePairs); // 获取键值对数组
        for (let i = 0; i < entries.length; i += batchSize) {
            const batch = entries.slice(i, i + batchSize); // 分割数组
            const pipeline = CacheManager.redisClient.pipeline(); // 创建管道

            for (const [key, value] of batch) {
                const stringValue = JSON.stringify(value); // 将值转换为 JSON 字符串
                if (ttl) {
                    pipeline.setex(key, ttl, stringValue); // 设置值并设置过期时间
                } else {
                    pipeline.set(key, stringValue); // 设置值
                }
            }

            await pipeline.exec(); // 执行管道
        }
        return true// 操作成功返回 true
    } catch (error) {
        console.error('setBulkValue Error:', error); // 错误处理
        return false// 操作失败返回 false
    }
}

9. 获取或设置缓存值

如果键不存在,则调用回调函数获取值并设置到缓存中。

public async getOrSetValue<T>(fallbackFn: () => Promise<T>, ttl: number = this.defaultTTL): Promise<T | null> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }

        const cachedValue = await this.getValue<T>(); // 尝试从缓存获取值

        if (cachedValue !== null) {
            return cachedValue; // 如果缓存中有值则直接返回
        }

        const value = await fallbackFn(); // 如果缓存中没有值则调用回调函数获取值
        await this.setValue(value, ttl); // 将值设置到缓存中
        return value; // 返回值
    } catch (error) {
        console.error('getOrSetValue Error:', error); // 错误处理
        return null// 操作失败返回 null
    }
}

10. 删除缓存值

删除当前键对应的缓存值。

public async delete(): Promise<boolean> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }
        await CacheManager.redisClient.del(this.currentKey); // 删除键
        return true// 操作成功返回 true
    } catch (error) {
        console.error('delete Error:', error); // 错误处理
        return false// 操作失败返回 false
    }
}

11. 检查键是否存在

检查当前键是否存在。

public async exists(): Promise<boolean> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }
        return (await CacheManager.redisClient.exists(this.currentKey)) === 1// 检查键是否存在
    } catch (error) {
        console.error('exists Error:', error); // 错误处理
        return false// 操作失败返回 false
    }
}

12. 获取键的剩余过期时间

获取当前键的剩余过期时间。

public async getTTL(): Promise<number> {
    try {
        if (!this.currentKey) {
            throw new Error('Key is required'); // 验证键是否已设置
        }
        return await CacheManager.redisClient.ttl(this.currentKey); // 获取 TTL
    } catch (error) {
        console.error('getTTL Error:', error); // 错误处理
        return -1// 操作失败返回 -1
    }
}

13. 断开连接

断开与 Redis 的连接。

public static async disconnect(): Promise<void> {
    if (CacheManager.redisClient) {
        await CacheManager.redisClient.quit(); // 断开连接
    }
}

三. 总结

  1. 通过上述缓存管理类的设计,我们实现了对 Redis 缓存的高效管理和操作,提升了 Node.js 应用的性能。
  2. 类支持单例模式、批量操作等功能,并且通过 TypeScript 提供了类型安全。
  3. 未来可以考虑添加更多高级特性,如动态数据库选择、更详细的错误处理机制等,进一步增强其功能性和稳定性。

通过使用此类缓存管理器,开发者可以更容易地集成缓存机制到他们的应用中,从而改善用户体验,减轻数据库压力。