Skip to content

Nest中使用Redis

我们通过一个简单的需求,来实现一下再Nest项目中应用Redis。

通过Redis缓存用户的购物车信息,当用户查询购物车信息时,首先从Redis中查询,如果缓存为空,再去MySQL中查询。当用户在购物车中增加商品数量时,需要现将更新保存到MySQL中,并通过更新到Redis,以确保缓存数据的一致性。

项目创建#

无论如何我们先建立项目:

Terminal window
nest n nest-redis -g -p pnpm

加下来,安装依赖包

Terminal window
pnpm add typeorm mysql2 @nestjs/typeorm redis -S

既然是购物车,先生成购物车模块

Terminal window
nest g res shopping-cart --no-spec

Redis初始化#

Redis通常会在多个模块中使用,为了更好的管理它,我们可以先创建一个redis模块,专门用来配置和导出Redis模块,其他模块可以通过依赖注入的方式使用它。甚至可以根据使用频率和需求,将模块定义为全局模块

Terminal window
nest g mo redis --flat

代码如下:

import { Module } from '@nestjs/common'
import { createClient } from 'redis'
const createRedisClient = () => {
return createClient({
socket: {
host: 'localhost',
port: 6379,
},
}).connect()
}
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: createRedisClient,
},
],
exports: ['REDIS_CLIENT'],
})
export class RedisModule {}

其中,createClient方法负责提供的Redis配置信息来注册Redis客户端,通过connect()方法与Redis服务建立连接。通过providers提供服务,这样在购物车的service中,我们就可以直接注入了:

@Injectable()
export class ShoppingCartService {
@Inject('REDIS_CLIENT')
private redisClient: RedisClientType
create(createShoppingCartDto: CreateShoppingCartDto) {
return this.redisClient.set('xxx', JSON.stringify(createShoppingCartDto))
}
}

xxx只是暂时命名,后面再统一处理

ORM处理#

在完善基础逻辑之前,数据库ORM相关内容需要先处理一下,首先当然还是在app.module.ts中初始化MySQL的连接

import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { RedisModule } from './redis.module'
import { ShoppingCartModule } from './shopping-cart/shopping-cart.module'
import { TypeOrmModule } from '@nestjs/typeorm'
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'nest_redis',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
RedisModule,
ShoppingCartModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

稍微完善一下ShoppingCart实体

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class ShoppingCart {
@PrimaryGeneratedColumn()
id: number
@Column()
userId: number
// 购物车数据,我们这里就简单保存一下购物车数量{count:1}就行了
@Column('json')
cartData: Record<string, number>
}

这个实体是用来和数据库打交道的对象映射实体,我们还需要传递数据,因此,dto数据我们也顺便添加了

create-shopping-cart.dto.ts

export class CreateShoppingCartDto {
userId: number
cartData: Record<string, number>
}

dto对象是专门用来传输数据的。

接下来在service中完善createfindOneupdate方法,用来添加、查询和更新购物车信息,并且保持Redis与MySQL数据的一致性。其实也就是在更新MySQL数据的时候,Redis缓存同时更新。

import { Inject, Injectable } from '@nestjs/common'
import { CreateShoppingCartDto } from './dto/create-shopping-cart.dto'
import { UpdateShoppingCartDto } from './dto/update-shopping-cart.dto'
import { RedisClientType } from 'redis'
import { Repository } from 'typeorm'
import { ShoppingCart } from './entities/shopping-cart.entity'
import { InjectRepository } from '@nestjs/typeorm'
@Injectable()
export class ShoppingCartService {
@Inject('REDIS_CLIENT')
private redisClient: RedisClientType
@InjectRepository(ShoppingCart)
private shoppingCartRepository: Repository<ShoppingCart>
async create(createShoppingCartDto: CreateShoppingCartDto) {
// 保存到mysql数据库中
await this.shoppingCartRepository.save(createShoppingCartDto)
// 保存到redis中
await this.redisClient.set(
`cart:${createShoppingCartDto.userId}`,
JSON.stringify(createShoppingCartDto)
)
return {
message: '添加购物车成功',
success: true,
}
}
async findOne(id: number) {
// 先从redis中获取数据缓存,没有再到mysql中获取
const data = await this.redisClient.get(`cart:${id}`)
const cartEntity = data ? JSON.parse(data) : null
if (cartEntity) {
return cartEntity
}
return this.shoppingCartRepository.findOne({
where: {
userId: id,
},
})
}
async update(updateShoppingCartDto: UpdateShoppingCartDto) {
const {
userId,
cartData: { count = 1 },
} = updateShoppingCartDto
// 查询数据
const cartEntity = await this.findOne(userId)
const cart = cartEntity ? cartEntity.cartData : {}
const quality = (cart.count || 0) + count
// 更新count
cart.count = quality
// 更新mysql数据
await this.shoppingCartRepository.update({ userId }, cartEntity)
// 更新redis缓存
await this.redisClient.set(`cart:${userId}`, JSON.stringify(cartEntity))
return {
message: '更新成功',
success: true,
}
}
}

**注意:**由于在ShoppingCart模块中用到了RedisModuleRepository,所以,必须在shopping-cart.module.ts中引入相应的模块才行

shopping-cart.module.ts

@Module({
imports: [RedisModule, TypeOrmModule.forFeature([ShoppingCart])],
controllers: [ShoppingCartController],
providers: [ShoppingCartService],
})
export class ShoppingCartModule {}

当然,controller上的代码我们稍作修改:

shopping-cart.controller.ts

import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common'
import { ShoppingCartService } from './shopping-cart.service'
import { CreateShoppingCartDto } from './dto/create-shopping-cart.dto'
import { UpdateShoppingCartDto } from './dto/update-shopping-cart.dto'
@Controller('shopping-cart')
export class ShoppingCartController {
constructor(private readonly shoppingCartService: ShoppingCartService) {}
@Post()
create(@Body() createShoppingCartDto: CreateShoppingCartDto) {
return this.shoppingCartService.create(createShoppingCartDto)
}
@Get(':userId')
findOne(@Param('userId') userId: string) {
return this.shoppingCartService.findOne(+userId)
}
@Patch()
update(@Body() updateShoppingCartDto: UpdateShoppingCartDto) {
return this.shoppingCartService.update(updateShoppingCartDto)
}
}

测试代码#

我们在APIFox中,添加一些测试数据

image-20250206135232067

这样,在Redis和Mysql中都会有相应的数据

image-20250206135346054

image-20250206135409597

我们可以多插入几条数据,方便一会查询修改

image-20250206135558112

接下来测试一下更新

image-20250206135853133

image-20250206135947807

image-20250206140439583

设置缓存有效期#

在实际开发中,Redis通常会设置缓存过期时间,以避免数据不一致或者缓存长时间未访问导致内存空间的浪费,比如,我们可以为更新设置缓存30秒的过期时间:

async update(updateShoppingCartDto: UpdateShoppingCartDto) {
const {
userId,
cartData: { count = 1 },
} = updateShoppingCartDto;
// 查询数据
const cartEntity = await this.findOne(userId);
const cart = cartEntity ? cartEntity.cartData : {};
const quality = (cart.count || 0) + count;
// 更新count
cart.count = quality;
// 更新mysql数据
await this.shoppingCartRepository.update({ userId }, cartEntity);
// 更新redis缓存
await this.redisClient.set(`cart:${userId}`, JSON.stringify(cartEntity), {
EX: 30,
});
return {
message: '更新成功',
success: true,
};
}

现在再更新一条数据,就可以从GUI上很清楚的看到有效期(TTL)从30开始倒计时了

image-20250206142623409

当然,为什么要设置缓存有效期,有下面的理由:

当然,设置缓存有效期并没有统一的最佳时长,这完全取决于具体的业务场景。通常可以根据下面的三种策略来选择: