caching with redis
Monday 11 March 2024

Caching with Redis

Why need caching?

  • Caching is a technique to store a copy of the data in a faster storage system (i.e. cache) to speed up the data retrieval.

  • Caching is used to store the data that the users frequently access. It helps reduce the load on the main storage system and improves the application’s performance.

What is Redis?

  • Redis is an open-source in-memory data structure store that can be used as a database, cache, and message broker.

  • It supports various data structures such as strings, hashes, lists, sets, sorted sets, bitmaps, hyperloglogs, and geospatial indexes with radius queries.

Start Redis Server

  • If you have not installed Redis, you can use https://console.upstash.com/ to create a Redis server.

  • Get the connection string from the dashboard.

Basic Node.js Application

  • Create a new node.js application with express.

  • Or you can use my node starter template Node-ENV

Install Redis Dependency and Connect to Redis

  • Install the redis package using yarn.
1yarn add ioredis @types/ioredis

note: i use yarn because it’s faster than npm and can install packages in parallel.

  • serve the Redis server connection string in the .env file or use the connection string directly in the code.

  • Create a new file redis.ts and connect to the Redis server.

 1import dotenv from 'dotenv';
 2import Redis from 'ioredis';
 3import { LOGGER } from '../logging';
 4
 5dotenv.config({ path: `${process.cwd()}/.env` });
 6
 7const { REDIS_URL } = process.env;
 8
 9if (!REDIS_URL) {
10 LOGGER.error('Please make sure that you have a valid Redis URL in your .env file');
11 process.exit(1);
12}
13
14export const client = new Redis(REDIS_URL);
15
16client.on('connect', () => {
17 LOGGER.info('Connected to Redis');
18});
19
20client.on('error', (err) => {
21 LOGGER.error(`Redis error: ${err}`);
22 process.exit(1);
23});

If you aren’t familiar with logging, you can use console.log instead.

Caching Functionality

  • Create a new file cache.ts and implement the caching functionality.
 1import mongoose, { Document } from 'mongoose';
 2import { client } from '../database/redis';
 3
 4declare module 'mongoose' {
 5 interface Query<ResultType, DocType, THelpers, RawDocType = DocType> {
 6  useCache: boolean;
 7  hashKey: string;
 8  cache(options?: { key?: string }): this;
 9 }
10}
11
12const exec: typeof mongoose.Query.prototype.exec = mongoose.Query.prototype.exec;
13
14mongoose.Query.prototype.cache = function (options = {}) {
15 this.useCache = true;
16 this.hashKey = JSON.stringify(options.key || '');
17 return this;
18};
19
20mongoose.Query.prototype.exec = async function <T extends Document>(): Promise<T[]> {
21 if (!this.useCache) {
22  return exec.call(this);
23 }
24
25 const key = JSON.stringify(
26  Object.assign({}, this.getQuery(), {
27   collection: this.model.collection.name,
28  })
29 );
30
31 const cacheValue = await client.hget(this.hashKey, key);
32
33 if (cacheValue) {
34  const doc = JSON.parse(cacheValue);
35  return Array.isArray(doc)
36   ? doc.map((d: any) => new this.model(d))
37   : [new this.model(doc)];
38 }
39
40 const result = await exec.call(this);
41 await client.hset(this.hashKey, key, JSON.stringify(result), 'EX', 1000);
42
43 return result;
44};
45
46export const clearHash = (hashKey: string): void => {
47 client.del(JSON.stringify(hashKey));
48};
  • Let’s understand the code above.

  • First, we are extending the mongoose.Query interface to add the cache method.

  • The cache method is used to enable caching for the query.

  • The hashKey is used to store the cache data in the Redis server.

  • The exec method is overridden to implement the caching functionality.

  • If the useCache is set to true, then we check if the data is available in the cache.

  • If the data is available in the cache, then we return the data from the cache.

  • If the data is not available in the cache, then we execute the original exec method and store the data in the cache.

  • The clearHash method is used to clear the cache data for a specific hashKey.

Clean Cache Middleware

  • Create a new file cleanCache.ts and implement the clean cache middleware.
1import { NextFunction, Response } from 'express';
2import { clearHash } from '../services/cache.service';
3
4export default async (req: Request, res: Response, next: NextFunction) => {
5 await next();
6 clearHash((req as any).user.id.toString());
7};
  • Let’s understand the code above.

  • The cleanCache middleware is used to clear the cache data after the request is completed.

  • await next() is used to execute the next middleware then we use the clearHash method to clear the cache data for the specific hashKey.

  • We are using the clearHash method to clear the cache data for the specific hashKey.

Backlinks