Skip to content

上节我们学了在 Nest 里操作 Redis,很多同学会问,为什么不用 cache-manager 呢?

官方文档里就是用的 cache-manger:

确实,cache-manger 有它好用的地方,但是它的缺点更多。

为什么这么说呢?

我们试一下就知道了:

nest new cache-manager-test

创建个 nest 项目。

进入项目,把它跑起来:

npm run start:dev

然后引入 cache-manager:

javascript
npm install @nestjs/cache-manager cache-manager

在 AppModule 注册下:

之后在 AppController 加几个路由:

注入 CacheManager,分别测试下它的 get、set、del 方法。

javascript
import { Controller, Get, Inject, Param, Query } from '@nestjs/common';
import { AppService } from './app.service';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Inject(CACHE_MANAGER)
  private cacheManager: Cache;

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('set')
  async set(@Query('value') value: string) {
    await this.cacheManager.set('kkk', value);
    return 'done'
  }

  @Get('get')
  async get() {
    return this.cacheManager.get('kkk');
  }

  @Get('del')
  async del() {
    await this.cacheManager.del('kkk');
    return 'done';
  }

}

浏览器访问下:

首先 get 没有数据:

然后 set 设置为 111:

再次 get 就有数据了:

之后 del 删掉:

再次 get 就为空了:

这个缓存的用法很简单。

此外,你还可以把它加到 handler 上,自动对结果缓存。

javascript
@Get('aaa')
@UseInterceptors(CacheInterceptor)
aaa(@Query('a') a: string){
    console.log('aaa', a);
    return 'aaa';
}

参数不变的情况下,刷新几次,可以看到控制台只打印了一次:

改变参数再刷新几次:

这时候控制台再次打印了,说明 handler 又被执行了。

其余的情况,都是直接拿缓存。

有同学说,缓存数据都在哪呢?好像也没引入 redis 呀?

确实,cache-manager 默认是在内存里的。

如果想存在 redis,要这样:

使用 cache-manager-redis-store,然后添加下连接配置:

javascript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';

import { RedisClientOptions } from 'redis';

@Module({
  imports: [
    CacheModule.register<RedisClientOptions>({
      // @ts-ignore
      store: async () => await redisStore({
        socket: {
          host: 'localhost',
          port: 6379,
        },
        database: 2
      }),
      host: 'localhost',
      port: 6379
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

这里的 cache-manager-redis-store 要安装下:

npm install cache-manager-redis-store

然后再次访问之前的 url:

这时候在 redis 里就可以看到缓存的数据:

注意,连接的时候我们指定 database为 2,所以在 RedisInsight 里要切到 db2 才可以看到这些数据。

缓存的过期时间也是可以配置的:

有同学会说,这个 CacheManager 不是很好用么?

确实,只做 set、get 的话,用 CacheManager 还是可以的,而且还有 CacheInterceptor 可以对接口做缓存。

但是,redis 还有 list、hash、zset 等数据结构,支持非常多的命令。

而用 CacheManager 的话,只支持 get、set:

这时候如果你想用 list、hash、zset 等数据结构,还是要自己封装。

那何必用 CacheManager 呢?

直接自己封装下 RedisService,实现 get、set 和其它操作,不是更香么?

有同学可能会说,但它带的 CacheInterceptor 也很方便呀。

确实,但我们一般不会对接口做缓存,如果要做的话,自己实现也不麻烦。

就是拼接这样一个 key 就好了:

我们实现下试试:

先安装用到的 redis 的包。

npm install redis

然后在 AppModule 添加一个自定义的 provider:

javascript
{
  provide: 'REDIS_CLIENT',
  async useFactory() {
    const client = createClient({
        socket: {
            host: 'localhost',
            port: 6379
        },
        database: 2
    });
    await client.connect();
    return client;
  }
}

然后创建一个 interceptor:

nest g interceptor my-cache --no-spec --flat

这样写:

javascript
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { RedisClientType } from 'redis';
import { of, tap } from 'rxjs';

@Injectable()
export class MyCacheInterceptor implements NestInterceptor {

  @Inject('REDIS_CLIENT')
  private redisClient: RedisClientType;

  @Inject(HttpAdapterHost)
  private httpAdapterHost: HttpAdapterHost;

  async intercept(context: ExecutionContext, next: CallHandler) {
    
    const request = context.switchToHttp().getRequest();

    const key = this.httpAdapterHost.httpAdapter.getRequestUrl(request);

    const value = await this.redisClient.get(key);

    if(!value) {
      return next.handle().pipe(tap((res) => {
        this.redisClient.set(key, res);
      }));
    } else {
      return of(value);
    }    
  }
}

redisClient 是我们前面创建的 provider,注入到这里来操作 redis。

HttpAdapterHost 前面没用过。我们知道 Nest 底层可以切换 express、fastify 等库,而这些库都会实现一个通用的适配器,就是 HttpAdapter。

我们这里用到这个适配器的方法来拿到 requestUrl,也就是 /aaa?a=3 这种。

具体的逻辑比较容易看懂,就是如果查到了 key,就直接返回 value,这里要返回 rxjs 的 Observable 对象,所以用 of 包一下。

否则,执行 handler 并且设置到 redis 里。

在 aaa 上应用我们自己写的 interceptor:

跑一下:

多次刷新只执行了一次 handler,并且在 redis 里存储了对应的 key、value。

这样,我们就自己实现了 CacheInterceptor。

回过头来看下,用 cache-manager 真的有必要么?

我觉得不如自己封装。

案例代码在小册仓库

总结

Nest 文档里操作 Redis 是通过 cache-manager,它可以切换内存存储和 redis 存储,支持 get、set,并且还有 CacheInterceptor 可以对接口做缓存。

但是它并不支持各种 Redis 的命令,绝大多数情况下是不够用的,需要自己再封装。

所以,不如干脆不用那个,自己连接 redis 然后操作它就好。

用到需要 CacheInterceptor 的话也可以自己实现。

后面我们操作 Redis 都是用自己封装个 RedisModule 的方式。