» Node.js:使用Express构建REST API » 2. 开发 » 2.9 缓存:Redis

缓存:Redis

在 MySQL 中的大查询或 MongoDB 中的大聚合可能需要几秒甚至几分钟才能完成。你绝对不希望频繁地触发这些操作。

将查询或聚合结果缓存到内存中是缓解这个问题的绝佳方法。如果你的 API 服务器在单个机器或节点上运行,只需将这些结果放入内存中的 HashMapsDictionaries 中即可解决问题。 但是,如果你有多台机器或节点运行 API 服务器并共享其公共内存的话,则 Redis 才是你的最佳选择。

尝试 Redis

  1. 在你的机器上安装 Redis 并启动它。

  2. 添加 redis 依赖。

npm i ioredis
  1. 更新代码。

添加 infrastructure/cache/helper.ts

export interface CacheHelper {
  save(key: string, value: string): Promise<void>;
  load(key: string): Promise<string | null>;
}

infrastructure/cache/redis.ts 中使用 redis:

import Redis, { RedisOptions } from "ioredis";

import { CacheConfig } from "@/infrastructure/config/config";
import { CacheHelper } from "./helper";

const defaultTTL = 3600; // seconds

export class RedisCache implements CacheHelper {
  private client: Redis;

  constructor(c: CacheConfig) {
    const options: RedisOptions = {
      host: c.host,
      port: c.port,
      password: c.password,
      db: c.db,
      commandTimeout: c.timeout,
    };
    this.client = new Redis(options);
    console.log("Connected to Redis");
  }

  async save(key: string, value: string): Promise<void> {
    await this.client.set(key, value, "EX", defaultTTL);
  }

  async load(key: string): Promise<string | null> {
    return await this.client.get(key);
  }

  close(): void {
    this.client.disconnect();
  }
}

导出如下内容,infrastructure/cache/index.ts

export { RedisCache } from "./redis";
export { CacheHelper } from "./helper";

添加相关配置项,在 infrastructure/config/config.ts 中:

@@ -11,9 +11,18 @@ interface ApplicationConfig {
   port: number;
 }
 
+export interface CacheConfig {
+  host: string;
+  port: number;
+  password: string;
+  db: number;
+  timeout: number; // in milliseconds
+}
+
 export interface Config {
   app: ApplicationConfig;
   db: DBConfig;
+  cache: CacheConfig;
 }
 
 export function parseConfig(filename: string): Config {

置入配置值,config.json

@@ -7,5 +7,12 @@
     "dsn": "mysql://test_user:test_pass@127.0.0.1:3306/lr_book?charset=utf8mb4",
     "mongo_uri": "mongodb://localhost:27017",
     "mongo_db_name": "lr_book"
+  },
+  "cache": {
+    "host": "localhost",
+    "port": 6379,
+    "password": "test_pass",
+    "db": 0,
+    "timeout": 5000
   }
 }

引入 redis 连接,application/wire_helper.ts

@@ -1,11 +1,13 @@
-import { MySQLPersistence, MongoPersistence } from "@/infrastructure/database";
 import { Config } from "@/infrastructure/config";
 import { BookManager, ReviewManager } from "@/domain/gateway";
+import { MySQLPersistence, MongoPersistence } from "@/infrastructure/database";
+import { RedisCache, CacheHelper } from "@/infrastructure/cache";
 
 // WireHelper is the helper for dependency injection
 export class WireHelper {
   private sql_persistence: MySQLPersistence;
   private no_sql_persistence: MongoPersistence;
+  private kv_store: RedisCache;
 
   constructor(c: Config) {
     this.sql_persistence = new MySQLPersistence(c.db.dsn);
@@ -13,6 +15,7 @@ export class WireHelper {
       c.db.mongo_uri,
       c.db.mongo_db_name
     );
+    this.kv_store = new RedisCache(c.cache);
   }
 
   bookManager(): BookManager {
@@ -22,4 +25,8 @@ export class WireHelper {
   reviewManager(): ReviewManager {
     return this.no_sql_persistence;
   }
+
+  cacheHelper(): CacheHelper {
+    return this.kv_store;
+  }
 }

假设列出所有图书操作需要在数据库中执行一个大查询,你需要将查询结果存入 Redis 以便下次可快速访问。

更改 application/executor/book_operator.ts

@@ -1,11 +1,16 @@
 import { BookManager } from "@/domain/gateway";
 import { Book } from "@/domain/model";
+import { CacheHelper } from "@/infrastructure/cache";
+
+const booksKey = "lr-books";
 
 export class BookOperator {
   private bookManager: BookManager;
+  private cacheHelper: CacheHelper;
 
-  constructor(b: BookManager) {
+  constructor(b: BookManager, c: CacheHelper) {
     this.bookManager = b;
+    this.cacheHelper = c;
   }
 
   async createBook(b: Book): Promise<Book> {
@@ -19,7 +24,13 @@ export class BookOperator {
   }
 
   async getBooks(): Promise<Book[]> {
-    return await this.bookManager.getBooks();
+    const cache_value = await this.cacheHelper.load(booksKey);
+    if (cache_value) {
+      return JSON.parse(cache_value);
+    }
+    const books = await this.bookManager.getBooks();
+    await this.cacheHelper.save(booksKey, JSON.stringify(books));
+    return books;
   }
 
   async updateBook(id: number, b: Book): Promise<Book> {

微调 adapter/router.ts

@@ -158,7 +158,7 @@ class RestHandler {
 // Create router
 function MakeRouter(wireHelper: WireHelper): express.Router {
   const restHandler = new RestHandler(
-    new BookOperator(wireHelper.bookManager()),
+    new BookOperator(wireHelper.bookManager(), wireHelper.cacheHelper()),
     new ReviewOperator(wireHelper.reviewManager())
   );
 
@@ -187,8 +187,12 @@ export function InitApp(wireHelper: WireHelper): express.Express {
   // Middleware to parse JSON bodies
   app.use(express.json());
 
-  // Use Morgan middleware with predefined 'combined' format
-  app.use(morgan("combined"));
+  // Use Morgan middleware with predefined tokens
+  app.use(
+    morgan(
+      ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" - :response-time ms'
+    )
+  );
 
   // Define a health endpoint handler
   app.get("/", (req: Request, res: Response) => {

morgan("combined") 日志格式没有响应时间,所以咱们需要自定义格式。

这些就是引入 redis 所需的调整。现在让我们试下缓存驱动的新端点。

用 curl 进行测试

列出所有图书:

curl -X GET http://localhost:3000/books

结果与之前相似,但是性能显著提升。你可以通过 Morgan 中间件的日志看出迹象。

::ffff:127.0.0.1 - - [02/Mar/2024:04:58:20 +0000] "GET /books HTTP/1.1" 200 483 "-" "curl/8.1.2" - 10.861 ms
::ffff:127.0.0.1 - - [02/Mar/2024:04:58:23 +0000] "GET /books HTTP/1.1" 200 483 "-" "curl/8.1.2" - 1.272 ms
::ffff:127.0.0.1 - - [02/Mar/2024:04:58:23 +0000] "GET /books HTTP/1.1" 200 483 "-" "curl/8.1.2" - 1.093 ms
::ffff:127.0.0.1 - - [02/Mar/2024:04:58:30 +0000] "GET /books HTTP/1.1" 200 483 "-" "curl/8.1.2" - 1.145 ms

使用 redis-cli 查看 Redis 中的值:

redis-cli

在 redis 客户端 shell 中调试这些键值:

127.0.0.1:6379> keys *
1) "lr-books"
127.0.0.1:6379> get lr-books
"[{\"id\":2,\"title\":\"Sample Book 222\",\"author\":\"John Doe\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-01T04:11:57.000Z\",\"updated_at\":\"2024-03-01T04:11:57.000Z\"},{\"id\":3,\"title\":\"Sample Book\",\"author\":\"John Doe\",\"published_at\":\"2023-01-01\",\"description\":\"A sample book description\",\"isbn\":\"1234567890\",\"total_pages\":200,\"created_at\":\"2024-03-01T04:40:16.000Z\",\"updated_at\":\"2024-03-01T04:40:16.000Z\"}]"
127.0.0.1:6379> del lr-books
(integer) 1

赞!Redis 已可以供君驱使了!💐