阶段一 · 基础命令与数据结构

五大数据结构(上)

String / List / Hash —— 最常用的三种数据类型,覆盖 80% 的 Redis 使用场景

Redis 学习笔记 · 第 02 篇 · 大纲

数据结构总览

Redis 的 value 不是简单的"字符串存储",而是支持多种数据结构,每种结构提供专属命令集。选对结构,事半功倍。

类型特征一句话类比
String二进制安全的字节序列,最大 512MB一个万能变量(存文本、数字、序列化对象)
List有序双向链表,允许重复一个双端队列(Deque)
Hashfield-value 映射表一行数据库记录 / 一个小 Map
Set无序唯一集合数学中的集合(下篇)
Sorted Set带分值的有序集合排行榜(下篇)
如果把 Redis 比作一个超级工具箱:String 是螺丝刀(万能但基础)、List 是传送带(先进先出/后进先出)、Hash 是收纳格(一个 key 下按字段分格存放)。选工具的核心原则:数据的访问模式决定选哪种结构

1. String(字符串)

String 是 Redis 最基础、最常用的数据类型。看名字像只能存文本,实际上它是二进制安全(Binary Safe)的——可以存文本、数字、JSON、序列化对象、甚至图片的 base64。

内部编码

Redis 根据 String 的实际内容自动选择编码:① 纯整数 → int(8字节直存);② 短字符串(≤44字节)→ embstr(连续内存分配);③ 长字符串 → raw(SDS 动态字符串)。你不需要手动选择,了解即可理解为什么 INCR 对整数值如此高效。

1.1 基础读写

SET

SET key value [EX seconds | PX ms] [NX | XX] [GET]

设置 key 的值。可选参数:EX/PX 设置过期时间;NX 仅 key 不存在时设置;XX 仅 key 存在时设置;GET 返回旧值(Redis 6.2+)。

GET

GET key

获取 key 的值。key 不存在返回 nil。仅适用于 String 类型。

SET greeting "Hello Redis"
OK

GET greeting
"Hello Redis"

# 设置时附带 10 秒过期
SET token "abc123" EX 10
OK

# 仅在 key 不存在时设置(原子操作,分布式锁基础)
SET lock:order_1001 "holder_A" NX EX 30
OK     ← 设置成功,拿到锁

SET lock:order_1001 "holder_B" NX EX 30
(nil)  ← key 已存在,设置失败

GETSET(已废弃)/ GETDEL / GETEX

GETDEL key
GETEX key [EX seconds | PERSIST]

GETDEL:获取并删除。GETEX:获取并修改过期时间(Redis 6.2+)。旧版 GETSET 已被 SET ... GET 取代。

1.2 数值操作

当 String 存储的是整数或浮点数时,Redis 提供原子性的加减操作——不需要先 GET 再 SET,一条命令搞定。

INCR / DECR

INCR key
DECR key

原子递增/递减 1。key 不存在时从 0 开始。值不是整数时报错。

INCRBY / DECRBY / INCRBYFLOAT

INCRBY key increment
DECRBY key decrement
INCRBYFLOAT key increment

按指定步长增减。INCRBYFLOAT 支持浮点数(如 INCRBYFLOAT price 0.5)。

# 计数器:文章阅读数
INCR article:1001:views       → (integer) 1
INCR article:1001:views       → (integer) 2
INCRBY article:1001:views 10  → (integer) 12

# key 不存在时自动初始化为 0 再操作
INCR new_counter              → (integer) 1

# 浮点数场景:商品价格调整
SET price "9.99"
INCRBYFLOAT price 0.50        → "10.49"
INCRBYFLOAT price -1.00       → "9.49"
INCR 就像高速公路的自动计数器——车过一辆加一,不需要有人先读出数字、加一、再写回去。这个"原子性"在高并发下极其重要:100 个线程同时 INCR,最终结果一定是精确的 +100。

1.3 批量操作与条件写入

MSET / MGET

MSET key1 value1 key2 value2 ...
MGET key1 key2 ...

批量设置/获取。一次网络往返完成多个操作,减少 RTT 开销。MSET 是原子的(要么全部设置,要么全不设置)。

# 一次写入多个用户信息
MSET user:1:name "Alice" user:1:age "28" user:1:city "Beijing"
OK

# 一次读取多个
MGET user:1:name user:1:age user:1:city
1) "Alice"
2) "28"
3) "Beijing"

# 不存在的 key 返回 nil
MGET user:1:name user:1:email
1) "Alice"
2) (nil)

MSETNX

MSETNX key1 value1 key2 value2 ...

仅当所有 key 都不存在时才批量设置。原子性:只要有一个 key 已存在,全部不设置。

APPEND / STRLEN

APPEND key value
STRLEN key

APPEND 在已有值末尾追加字符串(key 不存在则等同 SET)。STRLEN 返回值的字节长度。

MSET vs Pipeline

MSET 适合同类型批量写入。如果需要混合不同命令(SET + EXPIRE + INCR 等),应使用 Pipeline(流水线)打包发送,同样减少网络往返但更灵活。Pipeline 将在后续篇章详细讲解。

1.4 典型应用场景

场景实现方式关键命令
缓存 JSON 序列化后存入 String SET key json EX ttl
计数器 页面 PV、点赞数、库存扣减 INCR / DECRBY
分布式锁(基础版) SET NX + 过期时间 实现互斥 SET key val NX EX 30
限速器 固定窗口:INCR + EXPIRE INCR + EXPIRE
分布式 Session session_id → 用户数据 JSON SET sid data EX 1800
# 典型计数器:限制接口每分钟最多 100 次
# key = rate:user_123:api_create
INCR rate:user_123:api_create    → 1(第一次请求)
EXPIRE rate:user_123:api_create 60
# ... 后续请求
INCR rate:user_123:api_create    → 2, 3, ...
# 当返回值 > 100 时拒绝请求

2. List(列表)

List 是一个有序的字符串序列,允许重复元素。底层实现是 quicklist(ziplist 节点组成的双向链表),兼顾内存效率和操作性能。

List 就像一条双向传送带:你可以从左端放东西(LPUSH),也可以从右端放(RPUSH);取东西同理。这让它既能当(LIFO:同一端进出)又能当队列(FIFO:一端进另一端出)。

2.1 基础操作

LPUSH / RPUSH

LPUSH key element [element ...]
RPUSH key element [element ...]

从左端/右端插入一个或多个元素。key 不存在时自动创建空 List。返回操作后列表长度。

LPOP / RPOP

LPOP key [count]
RPOP key [count]

从左端/右端弹出元素。Redis 6.2+ 支持 count 参数一次弹出多个。列表为空时返回 nil。

# 构建一个任务队列
RPUSH tasks "task_a" "task_b" "task_c"    → (integer) 3

# 从左端取出(FIFO 队列行为)
LPOP tasks    → "task_a"
LPOP tasks    → "task_b"

# 列表当前状态:["task_c"]

# 多元素 LPUSH(注意插入顺序)
LPUSH stack "a" "b" "c"    → (integer) 3
# 列表状态:["c", "b", "a"]  ← 最后一个参数在最左端

LPUSH 的插入顺序

LPUSH key a b c 等价于依次执行 LPUSH key aLPUSH key bLPUSH key c。最终列表从左到右是 [c, b, a]。如果希望保持参数顺序,用 RPUSH

LLEN

LLEN key

返回列表长度。O(1) 复杂度(内部维护长度计数)。

LINDEX / LSET

LINDEX key index
LSET key index element

LINDEX 按下标获取元素(0-based,负数从右端计)。LSET 修改指定下标的值。注意:按下标访问是 O(N),列表很长时慎用。

2.2 范围查询与修剪

LRANGE

LRANGE key start stop

返回指定范围内的元素(闭区间)。LRANGE key 0 -1 返回全部元素。

RPUSH colors "red" "green" "blue" "yellow" "purple"

LRANGE colors 0 -1     → ["red","green","blue","yellow","purple"]
LRANGE colors 0 2      → ["red","green","blue"]
LRANGE colors -2 -1    → ["yellow","purple"]

LTRIM

LTRIM key start stop

只保留指定范围内的元素,其余全部删除。常用于维护固定长度的列表。

# 保留最近 100 条日志
LPUSH logs "2024-01-01 event_x"
LTRIM logs 0 99    ← 每次插入后修剪,列表永远不超过 100 条

LREM

LREM key count element

删除列表中与 element 相等的元素。count > 0:从左往右删最多 count 个;count < 0:从右往左;count = 0:删除全部匹配。

2.3 阻塞操作

普通的 LPOP/RPOP 在列表为空时立即返回 nil。但在消息队列场景中,消费者需要"等待"新消息到来——轮询浪费 CPU,这时需要阻塞版本

BLPOP / BRPOP

BLPOP key [key ...] timeout
BRPOP key [key ...] timeout

阻塞式弹出:列表非空时立即返回;列表为空时阻塞等待,直到有新元素被 PUSH 进来或超时(timeout=0 表示无限等待)。可同时监听多个 key。

# 终端 1(消费者)—— 阻塞等待
BLPOP job_queue 30
# ... 此处阻塞,等待中 ...

# 终端 2(生产者)—— 推送任务
RPUSH job_queue "send_email:user_42"

# 终端 1 立即收到响应:
1) "job_queue"              ← 来自哪个 key
2) "send_email:user_42"    ← 弹出的元素
BLPOP 就像餐厅叫号——你坐在那里等(阻塞),厨房做好一道菜(RPUSH)时喊你的号,你立刻取走。比起每隔 1 秒跑去柜台问"好了没"(轮询),阻塞等待既省力又实时。

BLPOP 的多 key 优先级

BLPOP key1 key2 key3 0 同时监听多个列表时,Redis 按参数顺序检查——谁先有数据就从谁那取。这可以用来实现优先级队列:高优先级的 key 放前面。

2.4 典型应用场景

场景实现模式关键命令
消息队列 生产者 RPUSH,消费者 BLPOP RPUSH + BLPOP
最近 N 条记录 LPUSH + LTRIM 固定长度 LPUSH + LTRIM
栈(LIFO) 同一端 PUSH 和 POP LPUSH + LPOP
Timeline 每人一个 List,关注的人发动态时 LPUSH LPUSH + LRANGE

List 做消息队列的局限

List 实现的队列是点对点(一条消息只能被一个消费者取走),不支持消费者组、消息确认(ACK)、重新投递。如果需要更完整的消息队列语义,Redis 5.0+ 的 Stream 类型是更好的选择(将在扩展数据类型篇讲解)。

3. Hash(哈希)

Hash 是一个 key 下的字段-值映射表(field → value)。适合存储对象——一个 key 代表一个实体,每个 field 是该实体的一个属性。

如果 String 是把整个对象 JSON.stringify 后塞进一个格子,那 Hash 就是给这个对象开了一排小抽屉:每个抽屉有标签(field),可以单独打开某个抽屉取/改,而不用把整个对象搬出来。修改单个字段时效率远高于 String 的"读-改-写"。

3.1 基础操作

HSET / HGET

HSET key field value [field value ...]
HGET key field

HSET 设置一个或多个 field 的值(Redis 4.0+ 支持多字段)。HGET 获取单个 field 的值。

# 存储一个用户对象
HSET user:1001 name "Alice" age "28" city "Beijing" role "engineer"
→ (integer) 4    ← 新增了 4 个 field

HGET user:1001 name     → "Alice"
HGET user:1001 age      → "28"
HGET user:1001 email    → (nil)    ← field 不存在

HDEL

HDEL key field [field ...]

删除一个或多个 field。返回实际删除的数量。

HEXISTS / HLEN

HEXISTS key field
HLEN key

HEXISTS 判断 field 是否存在(返回 0 或 1)。HLEN 返回 Hash 中 field 的数量。

3.2 批量操作与遍历

HMGET

HMGET key field [field ...]

一次获取多个 field 的值。不存在的 field 返回 nil。比多次 HGET 减少网络往返。

HGETALL / HKEYS / HVALS

HGETALL key
HKEYS key
HVALS key

HGETALL 返回所有 field 和 value(交替排列)。HKEYS 只返回所有 field 名。HVALS 只返回所有 value。

HMGET user:1001 name city role
1) "Alice"
2) "Beijing"
3) "engineer"

HGETALL user:1001
1) "name"
2) "Alice"
3) "age"
4) "28"
5) "city"
6) "Beijing"
7) "role"
8) "engineer"

HKEYS user:1001    → ["name","age","city","role"]
HVALS user:1001    → ["Alice","28","Beijing","engineer"]

HGETALL 的性能陷阱

当 Hash 内有大量 field(比如几万个)时,HGETALL 会一次返回全部数据,阻塞主线程且产生大量网络传输。此时应使用 HSCAN 渐进式遍历,与 KEYS vs SCAN 的道理一样。

HSCAN

HSCAN key cursor [MATCH pattern] [COUNT hint]

渐进式遍历 Hash 内的 field-value 对。用法与顶层 SCAN 一致。

3.3 数值操作

HINCRBY / HINCRBYFLOAT

HINCRBY key field increment
HINCRBYFLOAT key field increment

对 Hash 中某个 field 的值做原子加减。与 String 的 INCRBY 类似,但作用在单个 field 上。

# 购物车:商品数量增减
HSET cart:user_42 item_1001 2 item_1002 1
HINCRBY cart:user_42 item_1001 3    → (integer) 5    ← 2+3=5
HINCRBY cart:user_42 item_1001 -1   → (integer) 4    ← 减一件

# 浮点数场景
HSET product:1001 price "99.00"
HINCRBYFLOAT product:1001 price -10.50  → "88.5"

3.4 典型应用场景

场景实现方式优势
对象缓存 每个实体一个 Hash
user:{id} → field 为属性
可部分读取/更新,无需整体序列化
购物车 cart:{uid} → field 为商品 ID,value 为数量 增减数量用 HINCRBY,删除商品用 HDEL
用户配置 config:{uid} → field 为配置项名 读取/修改单个配置项 O(1)
聚合计数 stats:page_views → field 为页面 ID 所有页面的计数集中在一个 key,减少 key 数量

Hash vs String 存对象:怎么选?

维度String(JSON)Hash
读取方式只能整体读取整个 JSON可按 field 精确读取
修改单字段GET → 反序列化 → 修改 → 序列化 → SETHSET key field newVal 一条命令
内存效率字段少时 JSON 更紧凑field 少(<128个且值<64字节)时用 ziplist 编码,非常省内存
过期粒度整个 key 过期整个 key 过期(单个 field 不能独立过期)
适用场景对象整体读写、字段极少变动频繁修改部分字段、需要原子数值操作

实践经验

① 如果对象字段经常变动或需要单字段原子操作(如计数器),选 Hash;② 如果对象总是整体读写且字段固定,String + JSON 更简单;③ Hash 的单个 field 不支持独立设置 TTL——如果需要字段级过期,要么拆成多个 String key,要么在应用层处理。

快速回顾

类型核心命令记忆要点
String SET / GET 基础读写,SET 支持 EX/NX/XX 等选项
INCR / INCRBY 原子递增,计数器首选
MSET / MGET 批量操作减少 RTT
SET key val NX EX 分布式锁基础范式
List LPUSH / RPUSH 左/右端插入
LPOP / RPOP 左/右端弹出
BLPOP / BRPOP 阻塞式弹出,消息队列核心
LRANGE / LTRIM 范围查询 + 固定长度裁剪
Hash HSET / HGET 单 field 读写
HMGET / HGETALL 批量获取,大 Hash 用 HSCAN
HINCRBY field 级原子递增(购物车等)
HDEL / HEXISTS 删除/判断 field 是否存在

动手练习

  1. String 计数器:模拟文章阅读计数——创建 article:2001:views,用 INCR 累加 5 次,再用 DECRBY 减 2,验证最终值是否为 3。
  2. String 分布式锁:用 SET lock:resource NX EX 10 尝试加锁,然后再次执行同一命令验证返回 nil(锁已被持有)。等待过期后再试。
  3. List 队列:用 RPUSH 向 task_queue 推入 5 个任务,然后用 LPOP 逐个取出,观察 FIFO 顺序。
  4. List 固定长度:用 LPUSH + LTRIM 实现一个只保留最近 3 条消息的列表,连续推入 5 条后用 LRANGE 0 -1 确认只剩最新 3 条。
  5. Hash 对象:用 HSET 创建 product:5001(含 name、price、stock 字段),然后:
    • 用 HGET 读取 price
    • 用 HINCRBY 将 stock 减 1(模拟扣库存)
    • 用 HMGET 一次获取 name 和 stock
    • 用 HGETALL 查看完整对象
  6. (思考题)为什么 Redis 不支持对 Hash 中单个 field 设置过期时间?如果业务确实需要"部分字段过期"的效果,你会怎么设计?