快速入门 Redis 并在 Node.js 里操作它
提到数据库,大家可能会想到 mysql,它是通过表和字段来存储信息的,表和表之间通过 id 关联,叫做关系型数据库。
它提供了 sql 语言,可以通过这种语言来描述对数据的增删改查。
mysql 是通过硬盘来存储信息的,并且还要解析并执行 sql 语句,这些决定了它会成为性能瓶颈。
也就是说服务端执行计算会很快,但是等待数据库查询结果就很慢了。
那怎么办呢?
计算机领域最经常考虑到的性能优化手段就是缓存了。
能不能把结果缓存在内存中,下次只查内存就好了呢?
内存和硬盘的速度差距还是很大的:
所以做后端服务的时候,我们不会只用 mysql,一般会结合内存数据库来做缓存,最常用的是 redis。
因为需求就是缓存不同类型的数据,所以 redis 的设计是 key、value 的键值对的形式。
并且值的类型有很多:字符串(string)、列表(list)、集合(set)、有序集合(sorted set)、哈希表(hash)、地理信息(geospatial)、位图(bitmap)等。
我们分别来试一下。
redis 是分为服务端和客户端的,它提供了一个 redis-cli 的命令行客户端。
首先我们把 redis 服务跑起来。
通过 docker 的形式来跑:
下载一个 docker desktop 桌面端工具:
左边分别是镜像和镜像跑起来后的容器。
在搜索框搜索 redis,点击 run,把 redis 官方镜像下载并跑起来。
它会让你填一些容器的信息:
名字没啥好说的。
端口映射就是把主机的 6379 端口映射到容器内的 6379 端口,这样就能直接通过本机端口访问容器内的服务了。
volume 是数据卷,也就是挂载本地目录到容器的一个目录。
为什么要挂载数据卷呢?
因为镜像是不可修改的,每次运行都是全新的,但如果每次跑 redis 镜像,之前的数据都清空了可咋办?
所以要把本机的一个目录挂载上去,数据保存在本机。
跑起来之后是这样的:
容器内打印的日志说明 redis 服务跑起来了。
files 里可以看到所有的容器内的文件:
看到这个 mounted 的标志了没?
就代表这个目录是挂载的本地的一个目录。
我们在本地目录添加一个文件。
在容器内的 data 目录就能访问到这个文件了:
同样,在容器内修改了 data 目录,那本机目录下也会修改。
redis 服务跑起来之后,我们用 redis-cli 操作下。
在 terminal 输入 redis-cli,进入交互模式:
我们在这里做下 string 相关的操作:
文档里的命令有这么几个:
set、get 都挺简单:
incr 是用于递增的:
平时我们用的阅读量、点赞量等都是通过这个来计数的。
当我存了几个 key 后,可以通过 keys 来查询有哪些 key:
keys 后加一个模式串来过滤,常用的是 '*' 来查询所有 key。
然后再来看看 list。
这里我们切换成 GUI 工具吧,那个更直观一些。
这个就像 git 有人喜欢用命令行,有人喜欢用 GUI 工具一样。只是习惯问题,都可以。
我用的是 medis,一个 mac 上的开源 redis GUI 工具。
输入 ip 和端口之后点击连接:
就可以可视化看到所有的 key 和值:
同样也可以执行命令:
(不想装 GUI 工具的同学直接用 redis-cli 也是一样的,没啥区别)
然后我们继续看 list 类型的数据结构:
文档中有这么几个命令:
我们试一下:
lpush 是 left push 的意思,执行后会从左到右添加到列表中:
rpush 是 right push 的意思,执行后会从右往左添加到列表中:
(在 GUI 里要全选执行)
lpop 和 rpop 自然是从左边和从右边删除数据。
如果想查看数据呢?
在 GUI 里直接点开看就行,但在命令行里呢?
有同学说,不就是 get 么?
是不行的,get 只适用于 string 类型的数据,list 类型的数据要用 lrange。
输入一段 range,结尾下标为 -1 代表到最后。lrange list1 0 -1 就是查询 list1 的全部数据。
接下来我们再来看看 set:
set 的特点是无序并且元素不重复。
当我添加重复数据的时候:
只保留去重后的数据:
可以通过 sismember 判断是否是集合中的元素:
set 只能去重、判断包含,不能对元素排序。
如果排序、去重的需求,比如排行榜,可以用 sorted set,也就是 zset,:
它每个元素是有一个分数的:
会按照分数来排序:
通过 zrange 命令取数据,比如取排名前三的数据:
接下来是 hash:
和我们用的 map 一样,比较容易理解:
再就是 geo 的数据结构,就是经纬度信息,根据距离计算周围的人用的:
平时我们查找周围的人、周围的 xxx 都可以通过 redis 的 geospatial 数据结构实现。
redis 的数据结构就先介绍到这里。
是不是感觉还挺简单的。
确实,redis 学习成本挺低的,过一遍就会了。
不过一般 redis 的 key 我们会设置过期时间,通过 expire 命令:
比如我设置 dong2 的 key 为 3 秒过期:
3s 后就查不到了:
想查剩余过期时间使用 ttl:
一些有时效的数据可以设置个过期时间。
会了在命令行中操作 redis,重点还是要在代码里操作。
这就需要用 redis 的 node 的客户端了。
官方提供了 redis 这个 npm 包,我们安装一下:
npm install redis
然后在代码里连接 redis 服务,并执行命令:
import { createClient } from 'redis';
const client = createClient({
socket: {
host: 'localhost',
port: 6379
}
});
client.on('error', err => console.log('Redis Client Error', err));
await client.connect();
const value = await client.keys('*');
console.log(value);
await client.disconnect();
执行结果如下:
这里我能直接跑 es module 并且可以写顶层 await 是因为我在 package.json 里配置了 type: module。
我们再执行其他命令试试,比如 hset 创建一个 hash 表:
await client.hSet('guangguang1', '111', 'value111');
await client.hSet('guangguang1', '222', 'value222');
await client.hSet('guangguang1', '333', 'value333');
执行以后是这样的:
和我们在命令行客户端里操作一样。
这样我们就完成了 node 里操作 redis 的功能。
一般我们开发 node 应用会使用 nest 来写,那 nest 里怎么操作 redis 呢?
其实也是一样的:
执行 nest new nest-redis 创建一个 nest 项目:
它的 AppModule 是这样的:
我们给它添加一个 provider,返回 redis 的 client 实例。
当然,要先 npm install redis 安装 npm 包。
然后添加一个自定义的 provider:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { createClient } from 'redis';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: 'REDIS_CLIENT',
async useFactory() {
const client = createClient({
socket: {
host: 'localhost',
port: 6379
}
});
await client.connect();
return client;
}
}
],
})
export class AppModule {}
provider 通过工厂方法创建,token 为 REDIS_CLIENT。
然后注入到 service 里用就好了:
import { Inject, Injectable } from '@nestjs/common';
import { RedisClientType } from 'redis';
@Injectable()
export class AppService {
constructor(@Inject('REDIS_CLIENT') private redisClient: RedisClientType) {}
async getHello() {
const value = await this.redisClient.keys('*');
console.log(value);
return 'Hello World!';
}
}
这样就能在 nest 里操作 redis 了:
这就是 redis 的使用和在 nodejs 里操作 redis 的方式。
回到最开始的问题,我们完全可以查出数据来之后放到 redis 中缓存,下次如果 redis 有数据就直接用,没有的话就查数据库然后更新 redis 缓存。
这是 redis 的第一种用途,作为数据库的缓存。
第二种用途就是直接作为存储数据的地方了,因为 redis 本身是会做持久化的,也可以把数据直接保存在 redis 里,不存到 mysql。
当然,因为 redis 在内存存储数据,这样成本还是比较高的,需要经常扩容。
总结
因为 mysql 存在硬盘,并且会执行 sql 的解析,会成为系统的性能瓶颈,所以我们要做一些优化。
常见的就是在内存中缓存数据,使用 redis 这种内存数据库。
它是 key、value 的格式存储的,value 有很多种类型,比如 string、list、set、sorted set(zset)、hash、geopitial 等。
灵活运用这些数据结构,可以完成各种需求,比如排行榜用 zset、阅读数点赞数用 string、附近的人用 geopitial 等。
而且这些 key 都可以设置过期时间,可以完成一些时效性相关的业务。
通过 redis 的 npm 包就可以轻松的连接和操作 redis 了,如果在 nest 里,可以封装一个 provider,在里面使用 redis 的 npm 包创建连接。
redis 几乎和 mysql 一样是后端系统的必用中间件了,它除了用来做数据库的缓存外,还可以直接作为数据存储的地方。
学会灵活使用 redis,是后端开发很重要的一步。