为什么您应该多缓存少查询

还在一次又一次地调用相同的API吗?
这不仅效率低下——而且成本高昂。性能缓慢、成本更高,用户体验更差。
让我们停止这种做法——从这篇文章开始。:D
首先您需要了解Redis,简单来说,它是一个超快速的内存键值存储——非常适合缓存和减少后端负载。
有几种方式可以开始。
如果您正在开发,我建议使用Docker。
为此,您可以在CLI中运行:
  1. docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
这将启动Redis服务器。但查看实际存储的内容同样重要。为此,使用带有浏览器UI的完整Redis Stack:
  1. docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
现在,如果您访问 http://localhost:8001/redis-stack/browser
您会找到一个方便的控制面板,可以在其中可视化地存储、浏览和管理您的Redis数据:
要开始,安装redis包:
  1. npm i redis // 或 yarn, npm
现在,让我们设置一些类型和接口来保持代码的整洁和可扩展性。
我们将从一个用于标准化响应的通用枚举开始:
我们的枚举如下:
  1. # generic.interface.ts
  2. export enum StatusEnum {
  3. SUCCESS = 'success',
  4. ERROR = 'error'
  5. }
然后,为我们的缓存逻辑定义类型和结构:
  1. # cache.interface.ts
  2. import { StatusEnum } from './generic.interface';

  3. export type CacheCallbackType = Promise<{
  4. status: StatusEnum;
  5. data: any;
  6. error?: any;
  7. }>;

  8. export interface CacheInterface {
  9. key: string;
  10. callback: () => CacheCallbackType;
  11. configs?: {
  12. expirationInSeconds?: number;
  13. useUserId?: boolean;
  14. };
  15. }

  16. interface CacheReturnInterface {
  17. status: StatusEnum;
  18. data: any;
  19. }

  20. export type CacheReturnType = Promise<CacheReturnInterface>;
这些接口将帮助保持我们的代码类型化、一致且易于维护,随着项目的增长。
接下来,让我们设置一个辅助类来以整洁和可重用的方式处理Redis操作。
  1. import { createClient } from 'redis';

  2. export default class RedisHelper {
  3. private static client = createClient({
  4. username: 'default',
  5. password: process.env.REDIS_PASSWORD,
  6. socket: {
  7. host: process.env.REDIS_URL,
  8. port: parseInt(process.env.REDIS_PORT)
  9. }
  10. });

  11. static async connect() {
  12. if (!this.client.isOpen) {
  13. await this.client.connect();
  14. }

  15. return this;
  16. }

  17. static async get(key: string) {
  18. await this.connect();
  19. const value = await this.client.get(key);

  20. if (!value) {
  21. return value;
  22. }

  23. return value;
  24. }

  25. static async set(
  26. key: string,
  27. value: any,
  28. expirationInSeconds: number = 3600
  29. ) {
  30. await this.connect();
  31. await this.client.json.set(key, '
  32. , value);
  33. await this.client.expire(key, expirationInSeconds);

  34. return this;
  35. }

  36. static async del(key: string) {
  37. await this.connect();
  38. await this.client.del(key);
  39. }

  40. static async quit() {
  41. if (this.client.isOpen) {
  42. await this.client.quit();
  43. }
  44. }
  45. }
我们使用Redis的JSON命令(例如.json.set)来更直观地存储结构化数据。这比纯字符串给我们更多的灵活性——特别是在处理嵌套对象或数组时。
有了这个辅助类,我们可以:
仅在需要时连接到Redis(延迟连接)
设置带过期时间的值
轻松获取缓存数据
删除键
优雅地关闭连接
这个辅助类将处理任何类型请求的缓存——只要请求成功。它使用我们创建的RedisHelper与Redis通信。
在我的情况下,我通过将用户ID附加到缓存键来包含基于用户的缓存支持。在您的情况下,请随意调整此逻辑以适应您的需求。
  1. import {
  2. CacheInterface,
  3. CacheReturnType
  4. } from '../../interfaces/cache.interface';
  5. import { StatusEnum } from '../../interfaces/generic.interface';
  6. // import getUserId from '../default/userId';
  7. import RedisHelper from './RedisHelper';

  8. function getUserId () {
  9. return Math.random(); // 在这里使用您的逻辑!
  10. }

  11. export default class CacheHelper {
  12. // 检查缓存是否为空
  13. // 然后运行回调函数
  14. // 并用结果设置缓存
  15. static async checkCache({
  16. key,
  17. callback,
  18. configs
  19. }: CacheInterface): CacheReturnType {
  20. const cache = await RedisHelper.get(key);
  21. const expirationInSeconds = configs?.expirationInSeconds || 3600;

  22. // 仅在configs中指定时才在键中使用userId
  23. if (configs?.useUserId) {
  24. const userId = getUserId();
  25. key = `${key}:${userId}`;
  26. }

  27. if (cache) {
  28. return {
  29. status: StatusEnum.SUCCESS,
  30. data: cache
  31. };
  32. }

  33. try {
  34. const result = await callback();

  35. if (result.status === StatusEnum.ERROR) {
  36. return {
  37. status: StatusEnum.ERROR,
  38. data: result.data
  39. };
  40. }

  41. await RedisHelper.set(key, result.data, expirationInSeconds);

  42. return {
  43. status: StatusEnum.SUCCESS,
  44. data: result.data
  45. };
  46. } catch (error) {
  47. console.error('Error in checkCache:', error);

  48. return {
  49. status: StatusEnum.ERROR,
  50. data: null
  51. };
  52. }
  53. }

  54. // 辅助函数,始终在键中使用userId
  55. static async checkCacheWithId({
  56. key,
  57. callback,
  58. configs
  59. }: CacheInterface): CacheReturnType {
  60. return this.checkCache({
  61. key,
  62. callback,
  63. configs: {
  64. ...configs,
  65. useUserId: true
  66. }
  67. });
  68. }

  69. static async deleteCache(
  70. key: string,
  71. configs?: {
  72. useUserId?: boolean;
  73. }
  74. ): Promise<void> {
  75. if (configs?.useUserId) {
  76. const userId = getUserId();
  77. key = `${key}:${userId}`;
  78. }

  79. if (!key) {
  80. throw new Error('Key is required to delete cache');
  81. }

  82. try {
  83. await RedisHelper.del(key);
  84. } catch (error) {
  85. console.error('Error deleting cache:', error);
  86. throw error;
  87. }
  88. }

  89. static async deleteUserCache(key: string): Promise<void> {
  90. return this.deleteCache(key, { useUserId: true });
  91. }
  92. }
这个实用工具让您完全控制:
从缓存读取
如果需要,写入缓存
处理带或不带用户标识符的缓存键
轻松删除缓存条目
哦,redis中的标签看起来像这样:
  1. movies:user_123
  2. user:123:profile
  3. user:123:posts
  4. user:123:notifications
现在一切都设置好了,使用缓存变得简单而整洁。
以下是如何基于用户ID缓存/categories API请求的示例:
  1. import CacheHelper from './helpers/cache/CacheHelper';
  2. import { StatusEnum } from './interfaces/generic.interface';

  3. api.post('/categories', async (_req, res) => {
  4. const data = await CacheHelper.checkCacheWithId({
  5. key: 'categories',
  6. callback: async (): CacheCallbackType => {
  7. const data = await Categories();

  8. if (data.error) {
  9. return {
  10. status: StatusEnum.ERROR,
  11. data: data.error
  12. };
  13. }

  14. return {
  15. status: StatusEnum.SUCCESS,
  16. data
  17. };
  18. }
  19. });

  20. if (data.status === StatusEnum.ERROR) {
  21. return res.status(500).send({ error: data.data });
  22. }

  23. return res.status(200).send(data.data);
  24. });
这里发生了什么?
checkCacheWithId检查是否已经为该特定用户缓存了响应版本。
如果没有缓存,它运行回调(Categories()),保存结果并返回。
如果回调中有错误,它不会被缓存——而是返回错误。
结果快速而整洁地返回给客户端。
想要更进一步?您可以将此抽象为中间件或使用装饰器(如果您正在使用像NestJS这样的框架)。
如果您需要下一步的帮助,请告诉我!