我们通过一个简单的需求,来实现一下再Nest项目中应用Redis。
通过Redis缓存用户的购物车信息,当用户查询购物车信息时,首先从Redis中查询,如果缓存为空,再去MySQL中查询。当用户在购物车中增加商品数量时,需要现将更新保存到MySQL中,并通过更新到Redis,以确保缓存数据的一致性。
项目创建#
无论如何我们先建立项目:
nest n nest-redis -g -p pnpm加下来,安装依赖包
pnpm add typeorm mysql2 @nestjs/typeorm redis -S既然是购物车,先生成购物车模块
nest g res shopping-cart --no-specRedis初始化#
Redis通常会在多个模块中使用,为了更好的管理它,我们可以先创建一个redis模块,专门用来配置和导出Redis模块,其他模块可以通过依赖注入的方式使用它。甚至可以根据使用频率和需求,将模块定义为全局模块
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中完善create、findOne和update方法,用来添加、查询和更新购物车信息,并且保持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模块中用到了RedisModule和Repository,所以,必须在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中,添加一些测试数据

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


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

接下来测试一下更新



设置缓存有效期#
在实际开发中,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开始倒计时了

当然,为什么要设置缓存有效期,有下面的理由:
- **释放内存空间:**如果长时间不被访问或者更新,这部分缓存可能会持续占用大量的空间不被释放,这在一定程度上会导致Redis频繁扩容。设置过期时间可以自动释放内存供其他缓存使用
- 保证数据的实时性:当缓存对应的业务逻辑发生变更时,失效的缓存一直在内存中可能会导致业务逻辑错误,也就是我们常说的“脏数据”,这会影响系统的稳定性。设置自动过期可以保证缓存在一定时间是有效的,避免这种问题发生
- **保证数据的安全性:**过期时间其实是一种容错机制,缓存长时间存活在内存中,如果遇到内存泄漏或者恶意攻击,缓存中的隐私数据可能会被泄露
- **保证数据的一致性:**在并发场景或者缓存服务异常的时候,最新的缓存可能并未及时更新到内存中,此时获取到的旧缓存数据可能会因为数据不一致问题导致系统异常。设置缓存过期,可以保证缓存数据与数据库中的数据一致。
当然,设置缓存有效期并没有统一的最佳时长,这完全取决于具体的业务场景。通常可以根据下面的三种策略来选择:
- **短期缓存:**数据实时性要求高且频繁变动的情况下,可以这只较短的缓存时间,比如几分钟或者几个小时,以确保缓存数据及时与数据库同步,比如常见的新闻资讯推送、热点头条或者天气预报等等
- **中期缓存:**对于一些变动不频繁,但是要求一定实时性的数据,可以设置较长的缓存有效期,几小时或者几天,这样可以尽可能的减少数据库访问的压力。比如电商购物车、用户登录数据等等。
- **长期缓存:**对于相对稳定且变动少的数据,可以设置较长的有效期,几天或者几周。比如静态资源缓存、地理位置信息更新等等。