String / List / Hash —— 最常用的三种数据类型,覆盖 80% 的 Redis 使用场景
Redis 的 value 不是简单的"字符串存储",而是支持多种数据结构,每种结构提供专属命令集。选对结构,事半功倍。
| 类型 | 特征一句话 | 类比 |
|---|---|---|
| String | 二进制安全的字节序列,最大 512MB | 一个万能变量(存文本、数字、序列化对象) |
| List | 有序双向链表,允许重复 | 一个双端队列(Deque) |
| Hash | field-value 映射表 | 一行数据库记录 / 一个小 Map |
| Set | 无序唯一集合 | 数学中的集合(下篇) |
| Sorted Set | 带分值的有序集合 | 排行榜(下篇) |
String 是 Redis 最基础、最常用的数据类型。看名字像只能存文本,实际上它是二进制安全(Binary Safe)的——可以存文本、数字、JSON、序列化对象、甚至图片的 base64。
内部编码
Redis 根据 String 的实际内容自动选择编码:① 纯整数 → int(8字节直存);② 短字符串(≤44字节)→ embstr(连续内存分配);③ 长字符串 → raw(SDS 动态字符串)。你不需要手动选择,了解即可理解为什么 INCR 对整数值如此高效。
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 取代。
当 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"
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 将在后续篇章详细讲解。
| 场景 | 实现方式 | 关键命令 |
|---|---|---|
| 缓存 | 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 时拒绝请求
List 是一个有序的字符串序列,允许重复元素。底层实现是 quicklist(ziplist 节点组成的双向链表),兼顾内存效率和操作性能。
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 a、LPUSH key b、LPUSH 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),列表很长时慎用。
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:删除全部匹配。
普通的 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 的多 key 优先级
当 BLPOP key1 key2 key3 0 同时监听多个列表时,Redis 按参数顺序检查——谁先有数据就从谁那取。这可以用来实现优先级队列:高优先级的 key 放前面。
| 场景 | 实现模式 | 关键命令 |
|---|---|---|
| 消息队列 | 生产者 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 类型是更好的选择(将在扩展数据类型篇讲解)。
Hash 是一个 key 下的字段-值映射表(field → value)。适合存储对象——一个 key 代表一个实体,每个 field 是该实体的一个属性。
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 的数量。
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 一致。
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"
| 场景 | 实现方式 | 优势 |
|---|---|---|
| 对象缓存 | 每个实体一个 Hashuser:{id} → field 为属性 |
可部分读取/更新,无需整体序列化 |
| 购物车 | cart:{uid} → field 为商品 ID,value 为数量 |
增减数量用 HINCRBY,删除商品用 HDEL |
| 用户配置 | config:{uid} → field 为配置项名 |
读取/修改单个配置项 O(1) |
| 聚合计数 | stats:page_views → field 为页面 ID |
所有页面的计数集中在一个 key,减少 key 数量 |
| 维度 | String(JSON) | Hash |
|---|---|---|
| 读取方式 | 只能整体读取整个 JSON | 可按 field 精确读取 |
| 修改单字段 | GET → 反序列化 → 修改 → 序列化 → SET | HSET 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 是否存在 |
article:2001:views,用 INCR 累加 5 次,再用 DECRBY 减 2,验证最终值是否为 3。SET lock:resource NX EX 10 尝试加锁,然后再次执行同一命令验证返回 nil(锁已被持有)。等待过期后再试。task_queue 推入 5 个任务,然后用 LPOP 逐个取出,观察 FIFO 顺序。product:5001(含 name、price、stock 字段),然后: