mysql,它是通过表和字段来存储信息的,表和表之间通过 id 关联,叫做关系型数据库。
它提供了 sql 语言,可以通过这种语言来描述对数据的增删改查。
mysql 是通过硬盘来存储信息的,并且还要解析并执行 sql 语句,这些决定了它会成为性能瓶颈。
也就是说服务端执行计算会很快,但是等待数据库查询结果就很慢了。
那怎么办呢?
计算机领域最经常考虑到的性能优化手段就是缓存了。
能不能把结果缓存在内存中,下次只查内存就好了呢?
所以做后端服务的时候,我们不会只用 mysql,一般会结合内存数据库来做缓存,最常用的是 redis。
因为需求就是缓存不同类型的数据,所以 redis 的设计是 key、value 的键值对的形式。
并且值的类型有很多:字符串(string)、列表(list)、集合(set)、有序集合(sorted set)、哈希表(hash)、地理信息(geospatial)、位图(bitmap)等。
下面一一来学习下这些类型的使用。
string
对字符串的操作命令主要有set、get:
incr 是用于递增的:
平时我们用的阅读量、点赞量等都是通过这个来计数的。
当我存了几个 key 后,可以通过 keys 来查询有哪些 key:
keys 后加一个模式串来过滤,常用的是 * 来查询所有 key。
常用的命令有:
- set
- get
- incr
- keys
list
一个列表的结构。
lpush rpush
lpush list1 111
lpush list1 222
lpush list1 333
lpush 是 left push 的意思,执行后会从左到右添加到列表中。
rpush list1 444
rpush list1 555
rpush 是 right push 的意思,执行后会从右往左添加到列表中
lpop rpop
lpop 和 rpop 自然是从左边和从右边删除数据。
lpop list1
rpop list1
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
sismember set1 444
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)。
分为三步:
-
准备一个数组(柜子),先创建一个固定大小的数组,数组的每个位置叫 桶 ,可以放数据。
-
用哈希函数算 存放地址,当你要存一个数据(比如 key: "张三", value: "138xxxx")时:
- 把
key传入哈希函数,得到一个哈希值; - 把哈希值对数组长度取余,得到一个索引值(就是数组的下标,比如 3);
- 把
(key, value)存到数组的第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
hget hash1 key3
geo 数据结构
存储经纬度信息,根据距离计算周围的人用的。
geoadd loc 13.361389 38.115556 "guangguang" 15.087269 37.502669 "dongdong"
用 loc 作为 key,分别添加 guangguang 和 dongdong 的经纬度。
你会发现 redis 实际使用 zset 存储的,把经纬度转化为了二维平面的坐标:
你可以用 geodist 计算两个坐标点的距离:
geodist loc guangguang dogndong
用 georadius 搜索某个半径内的其他点,传入经纬度、半径和单位:
georadius loc 15 37 100 km
georadius loc 15 37 200 km
平时我们查找周围的人、周围的 xxx 都可以通过 redis 的 geo 数据结构实现。
过期时间
一般 redis 的 key 我们会设置过期时间,通过 expire 命令。
比如我设置 dong1 的 key 为 30 秒过期:
expire dogn1 30
等到了过期时间就会自动删除。
想查剩余过期时间使用 ttl:
总结
回到最开始的问题,我们完全可以查出数据来之后放到 redis 中缓存,下次如果 redis 有数据就直接用,没有的话就查数据库然后更新 redis 缓存。
这是 redis 的第一种用途,作为数据库的缓存,也是主要的用途。
第二种用途就是直接作为存储数据的地方了,因为 redis 本身是会做持久化的,能把内存中的数据持久化到磁盘上,避免重启后数据丢失。也就是可以把数据直接保存在 redis 里,不存到 mysql。
当然,因为 redis 在内存存储数据,这样成本还是比较高的,需要经常扩容。