nestjs学习6:认识redis

41 阅读8分钟

mysql,它是通过表和字段来存储信息的,表和表之间通过 id 关联,叫做关系型数据库。

它提供了 sql 语言,可以通过这种语言来描述对数据的增删改查。

mysql 是通过硬盘来存储信息的,并且还要解析并执行 sql 语句,这些决定了它会成为性能瓶颈。

也就是说服务端执行计算会很快,但是等待数据库查询结果就很慢了。

那怎么办呢?

计算机领域最经常考虑到的性能优化手段就是缓存了。

能不能把结果缓存在内存中,下次只查内存就好了呢?

所以做后端服务的时候,我们不会只用 mysql,一般会结合内存数据库来做缓存,最常用的是 redis。

因为需求就是缓存不同类型的数据,所以 redis 的设计是 key、value 的键值对的形式。

并且值的类型有很多:字符串(string)、列表(list)、集合(set)、有序集合(sorted set)、哈希表(hash)、地理信息(geospatial)、位图(bitmap)等。

下面一一来学习下这些类型的使用。

string

对字符串的操作命令主要有set、get:

c6666f2c01194f00ad7f45d33a7dfd47~tplv-k3u1fbpfcp-jj-mark_1512_0_0_0_q75.awebp

incr 是用于递增的:

fa3391bda1ff4967abecbd2a0da7e8c5~tplv-k3u1fbpfcp-jj-mark_1512_0_0_0_q75.awebp

平时我们用的阅读量、点赞量等都是通过这个来计数的。

当我存了几个 key 后,可以通过 keys 来查询有哪些 key:

image.png

keys 后加一个模式串来过滤,常用的是 * 来查询所有 key。

常用的命令有:

  • set
  • get
  • incr
  • keys

list

一个列表的结构。

lpush rpush

lpush list1 111
lpush list1 222
lpush list1 333

lpush 是 left push 的意思,执行后会从左到右添加到列表中。

image.png

rpush list1 444
rpush list1 555

rpush 是 right push 的意思,执行后会从右往左添加到列表中

image.png

lpop rpop

lpop 和 rpop 自然是从左边和从右边删除数据。

lpop list1
rpop list1

image.png

lrange

不就是 get 么?

是不行的,get 只适用于 string 类型的数据,list 类型的数据要用 lrange。

lrange list1 0 -1

输入一段 range,结尾下标为 -1 代表到最后。

lrange list1 0 -1 就是查询 list1 的全部数据。

set 数据类型

先复习下js中的set 类型。

Set 是 ES6 新增的无序集合,特点是:

  • 里面的值唯一,没有重复项(自动去重);
  • 可以存放任意类型的值(原始类型:数字、字符串、布尔;引用类型:对象、数组等);
  • 没有 的概念,只有

Set 是无序的,不能通过下标访问,那怎么访问呢?

  • 要么遍历 Set,逐个获取值(适合需要处理所有值的场景);

  • 要么把 Set 转换成数组,再用数组的方式取值(适合需要取指定位置值的场景);

  • 要么通过 has() 先判断值是否存在,再针对性处理(适合找特定值的场景)。

最常用场景如下:

// 场景1:数组去重(Set 最经典用法) 
const arr = [1, 2, 2, 3, 3, 3]; 
const uniqueArr = [...new Set(arr)]; 
// 用扩展运算符转成数组 
console.log(uniqueArr); // [1, 2, 3] 

// 场景2:存放不重复的用户 ID 
const userIds = new Set(); 
userIds.add("u001"); 
userIds.add("u002"); 
userIds.add("u001"); // 重复添加,无效
console.log(userIds.size); // 2 
console.log(userIds.has("u002")); // true 

// 场景3:遍历 Set 
for (const val of userIds) { 
   console.log(val); // 依次输出 u001、u002 
} 
// 也可以用 forEach
userIds.forEach(val => console.log(val));

redis 中的set 的特点也是无序并且元素不重复。

当我添加重复数据的时候:

sadd set1 111
sadd set1 111
sadd set1 111
sadd set1 222
sadd set1 222
sadd set1 333

结果只有 111 222 333。

可以通过 sismember 判断是否是集合中的元素:

sismember set1 111

image.png

sismember set1 444

image.png

zset 数据类型

set 只能去重、判断包含,不能对元素排序。

如果排序、去重的需求,比如排行榜,可以用 sorted set,也就是 zset

它每个元素是有一个分数的:

zadd zset1 5 guang
zadd zset1 4 dong
zadd zset1 3 xxx
zadd zset1 6 yyyy

会按照分数来排序。

通过 zrange 命令取数据,比如取排名前三的数据:

zrange zset1 0 2

hash 数据结构

先复习下什么是哈希函数及哈希表。

哈希函数(Hash Function)本质是一个 转换器 ,它接收任意长度的输入(比如一个字符串、一个数字、一个文件),输出一个固定长度、看起来无规律的结果,这个结果就叫 哈希值(Hash Value)。

前端中常用的哈希函数有MD5、SHA-256,比如 SHA-256 输出 256 位(64 个十六进制字符)。

就像快递站的 分区扫码器

  • 输入:你的快递单号(任意长度);
  • 哈希函数:扫码器的计算规则;
  • 输出:一个固定长度的分区编号(比如 05 区),你拿同一个单号扫,永远是 05 区;不同单号大概率扫出不同区,极少数情况会扫到同一个区(冲突)。

哈希表:一个按地址找东西的柜子

哈希表(Hash Table)也叫 散列表,是一种 数据结构,核心目的是 让查找数据的速度变快(理想情况下查找、插入、删除的时间复杂度都是 O (1)。

分为三步:

  1. 准备一个数组(柜子),先创建一个固定大小的数组,数组的每个位置叫 ,可以放数据。

  2. 用哈希函数算 存放地址,当你要存一个数据(比如 key: "张三", value: "138xxxx")时:

    • key 传入哈希函数,得到一个哈希值;
    • 把哈希值对数组长度取余,得到一个索引值(就是数组的下标,比如 3);
    • (key, value) 存到数组的第3个桶里。
  3. 查找数据时直接按地址找,当你要找张三的手机号时:

    • 再次用哈希函数算 张三的索引值(还是 3);
    • 直接去数组第 3 个桶里拿数据,不用遍历整个数组。

下面看看js中的map结构:

简单来说,Map 是 ES6 新增的键值对集合(类似对象 {}),但比普通对象更灵活、功能更强:

  • 普通对象的键只能是字符串 / 数字 / Symbol,而 Map 的键可以是任意类型(比如对象、数组、函数、null 等);

  • Map保留键的插入顺序,遍历的时候按插入顺序来;

  • Map 有原生的 size 属性(直接获取键值对数量),而对象需要手动计算;

  • Map 提供了 set()get()delete()clear() 等专用方法,操作更规范。

Map 的典型使用场景:

  • 需要非字符串 / 数字作为键的场景;
  • 需要频繁增删键值对的场景:

普通对象增删属性虽然也能做,但 Map 提供了更高效、语义更清晰的方法(delete()/clear()),且性能更好(尤其数据量大时)。

// 场景:临时存储用户会话数据,需要频繁添加/删除
const userSessions = new Map();

// 添加会话
userSessions.set('user123', { id: '123', expire: Date.now() + 3600000 });
userSessions.set('user456', { id: '456', expire: Date.now() + 3600000 });

// 删除单个会话
userSessions.delete('user123');

// 清空所有会话(比遍历删除对象属性高效)
userSessions.clear();
  • 需要有序遍历键值对的场景

Map 会严格按照键的插入顺序遍历,而普通对象(ES6 前)的键顺序是不确定的(数字键优先,字符串键次之)。

// 场景:按添加顺序展示用户操作日志
const operationLog = new Map();
operationLog.set('2026-01-24 10:00', '登录');
operationLog.set('2026-01-24 10:05', '查看订单');
operationLog.set('2026-01-24 10:10', '支付');

// 遍历:严格按插入顺序输出
for (const [time, action] of operationLog) {
  console.log(`${time}${action}`);
}

现在来看看redis中hash怎么用的。

hset hash1 key1 1
hset hash1 key2 2
hset hash1 key3 3
hset hash1 key4 4
hset hash1 key5 5

image.png

hget hash1 key3

image.png

geo 数据结构

存储经纬度信息,根据距离计算周围的人用的。

geoadd loc 13.361389 38.115556 "guangguang" 15.087269 37.502669 "dongdong" 

用 loc 作为 key,分别添加 guangguang 和 dongdong 的经纬度。

你会发现 redis 实际使用 zset 存储的,把经纬度转化为了二维平面的坐标:

image.png

你可以用 geodist 计算两个坐标点的距离:

geodist loc guangguang dogndong

image.png

用 georadius 搜索某个半径内的其他点,传入经纬度、半径和单位:

georadius loc 15 37 100 km
georadius loc 15 37 200 km

image.png

平时我们查找周围的人、周围的 xxx 都可以通过 redis 的 geo 数据结构实现。

过期时间

一般 redis 的 key 我们会设置过期时间,通过 expire 命令。

比如我设置 dong1 的 key 为 30 秒过期:

expire dogn1 30

等到了过期时间就会自动删除。

想查剩余过期时间使用 ttl:

image.png

总结

回到最开始的问题,我们完全可以查出数据来之后放到 redis 中缓存,下次如果 redis 有数据就直接用,没有的话就查数据库然后更新 redis 缓存。

这是 redis 的第一种用途,作为数据库的缓存,也是主要的用途。

第二种用途就是直接作为存储数据的地方了,因为 redis 本身是会做持久化的,能把内存中的数据持久化到磁盘上,避免重启后数据丢失。也就是可以把数据直接保存在 redis 里,不存到 mysql。

当然,因为 redis 在内存存储数据,这样成本还是比较高的,需要经常扩容。