Nest 操作 Redis
一、快速上手 Redis
1.1 安装和运行
Redis 官网提供了免费的 30MB 容量,可以直接注册使用。
还可以下载Redis Insight来查看和操作 Redis 数据库。
1.2 Redis 的常用命令
1.2.1 字符串操作
SET key value:设置键-值对GET key:获取键的值MGET key [key ...]:获取多个键的值DEL key:删除键-值对INCR key:将键的值增加1INCRBY key increment:将键的值增加指定的增量
1.2.2 列表操作
LPUSH key value [value ...]:将值推入列表左侧RPUSH key value [value ...]:将值推入列表右侧LPOP key [count]:从列表左侧弹出count个值,默认弹出一个RPOP key [count]:从列表右侧弹出count个值,默认弹出一个LLEN key:获取列表长度LRANGE key start stop:获取列表从下标为start开始,到下标stop结尾的值,如果stop为 -1,则表示结尾 。
1.2.3 集合操作
SADD key member [member ...]:向集合添加成员SREM key member [member ...]:移除集合中的成员SMEMBERS key:获取集合的所有成员SINTER key1 [key2 ...]:获取多个集合的交集
1.2.4 有序集合操作
ZADD key score member [score member ...]:向有序集合添加成员及其分数ZRANGE key start stop:按分数范围获取有序集合的成员
Sorted Set 也被称为 ZSet,是由一组按照分数排序并且成员唯一的数据组成。
1.2.5 哈希操作
HSET key field value [field value ...]:设置哈希字段的值HGET key field:获取哈希字段的值HGETALL key:获取哈希的所有字段和值
1.2.6 地理空间操作
-
GEOADD key longitude latitude member [longitude latitude member ...]:根据经纬度添加坐标成员 -
GEOPOS key [member [member ...]]:获取一个或多个成员的地理位置坐标 -
GEOSEARCH key FROMMEMBER member | FROMLONLAT longitude latitude BYRADIUS radius M | KM | FT | MI | BYBOX width height M | KM | FT | MI [ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]:根据不同条件获取成员坐标。例如查询距离这个坐标5km范围内的汽车:
GEOSEARCH share_cars FROMLONLAT -122.276 37.77 BYRADIUS 5km ASC -
GEODIST key member1 member2 [M | KM | FT | MI]:计算坐标成员之间的距离
1.2.7 位图操作
-
SETBIT key offset value:给指定偏移量的位设置0或1SETBIT user:1001:visit 10 1SETBIT user:1001:visit 20 1SETBIT user:1001:visit 25 1 -
GETBIT key offset:获取指定偏移量的位GETBIT user:1001:visit 20 -
BITCOUNT key [start end [BYTE | BIT]]:获取指定位为1的总数BIGCOUNT user:1001:visit
二、在 Nest 中使用 Redis 缓存
2.1 项目准备
nest new nest-redis -p pnpm
cd nest-redis/
pnpm i typeorm pg redis @nestjs/typeorm
nest g resource shopping-cart --no-spec
pnpm start:dev
2.2 Redis 初始化
创建一个 redis.module.ts 文件,专门用来配置和导出 Redis 模块,其他模块可以通过依赖注入的方式使用它,也可以把 Redis 定义为全局模块,这里采用局部模块的方式。
// src/redis/redis.module.ts
import { Module } from '@nestjs/common';
import { createClient } from 'redis';
const createRedisClient = () => {
// 注册客户端
return createClient({
username: 'default',
password: '',
socket: {
host: 'redis-13396.crce264.ap-east-1-1.ec2.cloud.redislabs.com',
port: 13396,
},
// 建立连接
}).connect();
};
@Module({
providers: [
{
// 定义一个 token 为 NEST_REDIS 的依赖项
provide: 'NEST_REDIS',
// useFactory 属性定义一个工厂方法,用于创建并返回 Redis 客户端实例
useFactory: createRedisClient,
},
],
// 将 Redis 客户端导出,使其成为可以在其他模块中使用的共享对象
exports: ['NEST_REDIS'],
})
export class RedisModule {}
在 shopping-cart.module.ts 文件中导入 RedisModule
import { Module } from '@nestjs/common';
import { ShoppingCartService } from './shopping-cart.service';
import { ShoppingCartController } from './shopping-cart.controller';
import { RedisModule } from 'src/redis/redis.module';
@Module({
controllers: [ShoppingCartController],
providers: [ShoppingCartService],
imports: [RedisModule],
})
export class ShoppingCartModule {}
在 shopping-cart.service.ts 文件中通过 @Inject 装饰器导入并使用 Redis 客户端
...
import { Injectable, Inject } from '@nestjs/common';
import type { RedisClientType } from 'redis';
@Injectable()
export class ShoppingCartService {
@Inject('NEST_REDIS')
private redisClient: RedisClientType;
async create(createShoppingCartDto: CreateShoppingCartDto) {
await this.redisClient.set('key', JSON.stringfy(createShoppingCartDto));
}
}
...
2.3 建表并构建缓存
在 app.module.ts 中初始化 MySQL 连接
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ShoppingCartModule } from './shopping-cart/shopping-cart.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
// 初始化 PostgreSQL 连接
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'root',
database: 'nest_redis',
entities: [__dirname + '/**/*.entity.{.ts, .js}'],
autoLoadEntities: true,
synchronize: true,
}),
ShoppingCartModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
完善 shopping-cart.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class ShoppingCart {
@PrimaryGeneratedColumn()
id: number;
@Column()
userId: number;
@Column({ type: 'json' })
cartData: Record<string, number>;
}
在 shopping-cart.service.ts 中添加 create 、findOne 和 update 方法
import { Injectable, Inject } from '@nestjs/common';
import { CreateShoppingCartDto } from './dto/create-shopping-cart.dto';
import { UpdateShoppingCartDto } from './dto/update-shopping-cart.dto';
import type { RedisClientType } from 'redis';
import { InjectRepository } from '@nestjs/typeorm';
import { ShoppingCart } from './entities/shopping-cart.entity';
import { Repository } from 'typeorm';
@Injectable()
export class ShoppingCartService {
@Inject('NEST_REDIS')
private redisClient: RedisClientType;
@InjectRepository(ShoppingCart)
private shoppingCartRepository: Repository<ShoppingCart>;
async create(createShoppingCartDto: CreateShoppingCartDto) {
// 保存到 db 中
await this.shoppingCartRepository.save(createShoppingCartDto);
// 更新 redis 缓存
await this.redisClient.set(
`cart:${createShoppingCartDto.userId}`,
JSON.stringify(createShoppingCartDto),
);
return {
msg: '添加成功!',
success: true,
};
}
async findOne(id: number): Promise<ShoppingCart | null> {
// 先从 Redis 中查询缓存,没有再查 db
const data = await this.redisClient.get(`cart:${id}`);
const cartEntity = data ? (JSON.parse(data) as ShoppingCart) : null;
if (cartEntity) return cartEntity;
return await this.shoppingCartRepository.findOne({
where: {
userId: id,
},
});
}
async update(updateShoppingCartDto: UpdateShoppingCartDto) {
const { userId, cartData } = updateShoppingCartDto;
const count = cartData!.count;
// 查询数据
const cartEntity = (await this.findOne(userId as number)) as ShoppingCart;
const cart = cartEntity ? cartEntity.cartData : {};
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const quality = (cart.count || 0) + count;
// 更新 count
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
cart.count = quality;
// 更新 db 数据
await this.shoppingCartRepository.update({ userId }, cartEntity);
// 更新 Redis 缓存
await this.redisClient.set(`cart:${userId}`, JSON.stringify(cartEntity));
return {
msg: '更新成功!',
success: true,
};
}
}
在 create-shopping-cart.dto.ts 中定义接口字段
export class CreateShoppingCartDto {
userId: number;
cartData: Record<string, number> & { count: number };
}
2.4 运行代码
### 添加购物车
POST http://localhost:3000/shopping-cart HTTP/1.1
Content-Type: application/json
{
"userId": 2,
"cartData": {
"count": 1
}
}
### 更新购物车数量
PATCH http://localhost:3000/shopping-cart/1
Content-Type: application/json
{
"userId": 1,
"cartData": {
"count": 10
}
}
### 查找用户的购物车数量
GET http://localhost:3000/shopping-cart/1 HTTP/1.1
2.5 设置缓存有效期
实际业务中,Redis 通常会设置缓存过期时间,以避免数据不一致或缓存长时间未访问(更新)导致内存空间浪费等问题。
// shopping-cart.service.ts
...
// 更新 redis 缓存
await this.redisClient.set(
`cart:${createShoppingCartDto.userId}`,
JSON.stringify(createShoppingCartDto),
{
EX: 30, // 30秒过期
},
);
...
设置缓存有效期有如下理由:
- 释放内存空间:不释放会持续占用大量的空间,一定程度上会导致
Redis频繁扩容。 - 保证数据的实时性:当缓存对应的业务逻辑发生变更,失效的缓存可能会导致业务逻辑出错,这会影响系统的稳定性。
- 保证数据的安全性:缓存长时间存活于内存中,如果遇到内存泄露或恶意软件攻击,缓存中的隐私数据有可能会泄露。
- 保证数据的一致性:在并发场景或缓存服务异常时,最新缓存有可能并未更新到内存中,此时获取到的旧缓存数据可能会因为数据不一致问题导致系统异常。
2.6 选择合理的有效期
- 短期缓存:在数据实时性要求高且频繁变动的情况下,可以设置较短的缓存时间,如几分钟或几小时,以确保缓存数据及时与数据库同步。这常见于新闻资讯推送、热点头条及天气预报等。
- 中期缓存:对于一些变动不频繁,但要求具有一定实时性的数据,可以设置较长的缓存有效期,如几小时或几天,以尽可能减轻数据库的访问压力。这常见于电商购物数据、用户登录数据等。
- 长期缓存:对于相对稳定且变动少的数据,可以设置较长的有效期,如几天或几周。常见于静态资源缓存、地理位置信息更新等。