1. 先看 Redis 数据类型
Redis 常见的一个误解是:它不是只能存字符串的 key-value 服务。Redis 的核心价值在于数据结构丰富,应用可以用很少的代码完成计数、排行、集合运算、状态统计、地理位置计算和简单消息流。
常见类型如下:
| 类型 | 结构特点 | 常见场景 |
|---|---|---|
| String | 一个 key 对应一个字符串、整数或二进制内容 | 缓存、计数器、验证码、分布式锁 |
| List | 有序列表,支持左右两端插入和弹出 | 消息列表、简单队列、最新动态 |
| Hash | 一个 key 下存多个 field-value | 用户对象、购物车、配置项 |
| Set | 无序、不重复集合 | 标签、抽奖、点赞、共同好友 |
| ZSet | 带 score 的有序集合 | 排行榜、热榜、延迟队列 |
| Bitmap | 基于 String 的位操作 | 签到、活跃状态、布尔统计 |
| HyperLogLog | 基数估算结构 | UV 估算、去重计数 |
| GEO | 经纬度位置索引 | 附近门店、附近的人 |
| Stream | 日志型消息流 | 消息队列、消费组 |
| Bitfield | 以位宽读写整数 | 紧凑型状态编码 |
2. Key 通用命令
无论 value 是什么类型,key 层面的命令都通用。
| 命令 | 说明 |
|---|---|
| KEYS pattern | 查找匹配 key。生产环境慎用大范围 KEYS * |
| SCAN cursor [MATCH pattern] [COUNT n] | 渐进式扫描 key,生产环境更推荐 |
| EXISTS key | 判断 key 是否存在 |
| TYPE key | 查看 key 对应 value 类型 |
| DEL key [key …] | 同步删除 key |
| UNLINK key [key …] | 异步删除 key,适合大 key |
| TTL key | 查看剩余过期秒数,-1 表示不过期,-2 表示不存在 |
| EXPIRE key seconds | 设置过期时间 |
| SELECT db | 切换数据库,默认 0 |
| DBSIZE | 查看当前库 key 数量 |
| FLUSHDB | 清空当前库 |
| FLUSHALL | 清空所有库 |
示例:
set user:1:name zhangsan
exists user:1:name
type user:1:name
expire user:1:name 60
ttl user:1:name
unlink user:1:name
这里有两个细节:
- Redis 命令不区分大小写,但 key 区分大小写。
- 大量 key 查询不要直接用 KEYS *,应该用 SCAN 分批扫。
KEYS 会在 Redis 主线程里扫描匹配 key,数据量大时会阻塞其它请求。在 Cluster 模式下,虽然命令只作用于单个节点,但每个节点仍然可能被阻塞,所以影响更隐蔽。SCAN 更适合生产排查,但它是渐进式游标扫描,不保证一次遍历期间绝对没有重复或遗漏;如果 key 在扫描过程中持续变化,业务侧要能接受重复处理或做去重。
3. String 字符串
String 是 Redis 最基础的数据类型,一个 key 对应一个 value。value 可以是普通字符串、数字字符串,也可以是二进制内容。从实现语义上看,Redis string 是 binary-safe 字节数组,不要求内容必须是文本。
3.1 常用命令
| 命令 | 说明 |
|---|---|
| SET key value | 设置值 |
| GET key | 获取值 |
| MSET key value [key value …] | 批量设置 |
| MGET key [key …] | 批量获取 |
| SETNX key value | key 不存在时设置 |
| SETEX key seconds value | 设置值并指定过期时间 |
| SET key value NX EX seconds | 常用于分布式锁的基础写法 |
| INCR key | 自增 1 |
| INCRBY key increment | 增加指定整数 |
| DECR key | 自减 1 |
| DECRBY key decrement | 减少指定整数 |
| STRLEN key | 字符串长度 |
| APPEND key value | 追加内容 |
| GETRANGE key start end | 截取指定范围 |
| SETRANGE key offset value | 从 offset 开始覆盖 |
3.2 SET 命令及常用参数说明
Redis 中的 SET 命令用于设置指定 key 的 value。
基本语法如下:
SET key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
SET 命令支持多个可选参数,包括 EX、PX、NX、XX、KEEPTTL、GET、EXAT、PXAT 等。
其中:
- EX seconds:以秒为单位设置过期时间。
- PX milliseconds:以毫秒为单位设置过期时间。
- EXAT timestamp:设置以秒为单位的 UNIX 时间戳所对应的时间为过期时间。
- PXAT milliseconds-timestamp:设置以毫秒为单位的 UNIX 时间戳所对应的时间为过期时间。
- NX:只有在 key 不存在时才设置键值。
- XX:只有在 key 已存在时才设置键值。
- KEEPTTL:保留设置前指定 key 的生存时间。
- GET:返回指定 key 原来的值,如果 key 不存在则返回 nil。
需要注意的是,SET key value EX seconds 可以替代 SETEX,SET key value PX milliseconds 可以替代 PSETEX,SET key value NX 可以覆盖 SETNX 的基础语义。分布式锁场景推荐使用 SET key value NX EX seconds,不要使用 SETNX 后再单独 EXPIRE 的两步写法,否则进程在两步之间异常退出时可能留下没有过期时间的锁。
根据官方文档的描述,未来版本中 SETEX、PSETEX、SETNX 命令可能会被淘汰,建议优先使用 SET 命令配合对应参数完成操作。
另外,EXAT、PXAT 以及 GET 是 Redis 6.2 新增的可选参数。
示例:
set counter 1
incr counter
incrby counter 10
get counter
set captcha:1001 839201 ex 120
ttl captcha:1001
3.3 分布式锁的基础写法
最基础写法是:
set lock:order:1001 request-id-001 nx ex 30
含义是:
| 参数 | 说明 |
|---|---|
| NX | key 不存在才写入 |
| EX 30 | 30 秒后自动过期 |
| request-id-001 | 锁持有者标识,释放锁时需要校验 |
分布式锁命令参数示意:
set key value [EX seconds] [PX milliseconds] [NX|XX]
# EX:key 在多少秒之后过期
# PX:key 在多少毫秒之后过期
# NX:当 key 不存在的时候,才创建 key,效果等同于 setnx
# XX:当 key 存在的时候,覆盖 key
这里只讲基础命令,真正生产级分布式锁还要考虑持有者校验、Lua 原子释放、续期、业务幂等问题,不能只用 DEL lock 粗暴释放。释放锁时必须先比较 value 是否仍然是自己的 request id,再删除 key;比较和删除要放在同一个 Lua 脚本里执行,避免误删其它客户端重新获得的锁。
3.4 使用场景
String 常见场景:
- 缓存对象 JSON
- 验证码和登录态
- 阅读/访问计数器
- 简单开关配置
- 分布式锁的基础结构
访问计数示意:
# 比如访问商品次数,点一下加一次
127.0.0.1:6379> INCR items:1
(integer) 1
127.0.0.1:6379> INCR items:1
(integer) 2
127.0.0.1:6379> INCR items:1
(integer) 3
4. List 列表
List 是一种有序列表,元素会按照插入顺序进行排列。
从结构上看,List 可以理解为一个双端队列,支持从左、右两端快速插入和弹出元素。它常用的操作包括 push 和 pop,因此非常适合用在栈、队列、消息队列等场景中。Redis 7 中 List 的底层主要基于 quicklist/listpack 这类紧凑结构实现,不再按老资料里的 ziplist 术语理解。
Redis List 的容量非常大,最多可以存储:
2^32 - 1
个元素,也就是大约 40 多亿 个元素。
List 的基本特点如下:
- left 和 right 两端都可以插入元素;
- 如果 key 不存在,执行插入操作时会创建新的 List;
- 如果 key 已经存在,执行插入操作时会在原有 List 中新增内容;
- 如果 List 中的元素被全部移除,对应的 key 也会自动消失。
需要注意的是,List 对两端的插入和弹出操作性能很高,但是如果通过索引下标操作中间位置的元素,性能会相对较差。
因此,在实际使用中,Redis List 更适合用于:
- 栈结构;
- 队列结构;
- 简单消息队列;
- 最新消息列表;
- 任务排队处理。

4.1 常用命令
| 命令 | 说明 |
|---|---|
| LPUSH key element [element …] | 从左侧插入 |
| RPUSH key element [element …] | 从右侧插入 |
| LRANGE key start stop | 按范围读取 |
| LPOP key [count] | 从左侧弹出 |
| RPOP key [count] | 从右侧弹出 |
| LINDEX key index | 按索引读取 |
| LLEN key | 获取长度 |
| LREM key count element | 删除指定元素 |
| LTRIM key start stop | 截取并保留指定范围 |
| LSET key index element | 修改指定下标 |
| LINSERT key BEFORE|AFTER pivot element | 在指定元素前后插入 |
示例:
rpush msg:list m1 m2 m3
lrange msg:list 0 -1
lpop msg:list
rpop msg:list
llen msg:list
RPOPLPUSH 在老资料里很常见,新版本更推荐用 LMOVE 表达从一个 list 移动到另一个 list:
lmove source-list target-list right left
LMOVE 可以显式指定源列表和目标列表的 LEFT/RIGHT 方向,比 RPOPLPUSH 更清晰。
4.2 使用场景
Redis 的 List 是一个有序列表,支持从左、右两端插入和弹出数据,所以它非常适合用来保存有顺序要求的数据。
List 常用来做:
- 最新消息列表
- 简单队列
- 文章评论列表
- 公众号消息列表
- 任务排队处理
- 简单的订阅消息列表
4.2.1 最新消息列表
比如微博、朋友圈、公众号文章列表等场景,都可以使用 List 保存最新发布的内容。
例如将最新文章 ID 插入到列表左侧:
LPUSH article:list 1001
LPUSH article:list 1002
LPUSH article:list 1003
查询最新的 10 条文章:
LRANGE article:list 0 9
因为 List 是有序的,所以可以按照插入顺序保存数据。通常最新的数据放在左侧,查询时从左侧开始取。
4.2.2 简单队列
List 也可以用来实现简单队列。
生产者从左侧写入任务:
LPUSH task:queue task1
LPUSH task:queue task2
LPUSH task:queue task3
消费者从右侧取出任务:
RPOP task:queue
这样就可以形成一个先进先出的队列结构:
左侧写入,右侧取出
LPUSH + RPOP
如果希望消费者在没有任务时阻塞等待,可以使用:
BRPOP task:queue 0
其中 0 表示一直阻塞等待,直到队列中有数据可以取出。
阻塞式队列要注意消费者数量和超时策略。大量消费者长时间阻塞在同一个 key 上,会增加连接和调度压力;如果多个客户端同时被唤醒争抢任务,也可能出现类似惊群的效果。生产队列还要考虑 ACK、重试和死信处理,这些能力 List 本身不完整。
4.2.3 文章评论列表
List 也可以用于保存文章评论 ID。
例如一篇文章下面有多条评论,可以把评论 ID 存入 List:
LPUSH article:1001:comments comment:1
LPUSH article:1001:comments comment:2
LPUSH article:1001:comments comment:3
查询文章下的最新评论:
LRANGE article:1001:comments 0 9
这种方式适合评论数量不是特别复杂、只需要按照时间顺序展示的场景。
如果评论需要支持点赞数排序、热评排序,则可以考虑使用 ZSet。
4.2.4 公众号消息列表
公众号消息、系统通知、站内信等也可以使用 List 保存。
例如给某个用户推送消息:
LPUSH user:1001:messages msg:1
LPUSH user:1001:messages msg:2
LPUSH user:1001:messages msg:3
用户打开消息中心时,查询最近的消息:
LRANGE user:1001:messages 0 19
如果只想保留最近 100 条消息,可以配合 LTRIM 使用:
LTRIM user:1001:messages 0 99
这样可以避免 List 无限增长,节省 Redis 内存。
4.2.5 订阅消息场景
假设用户订阅了某个公众号,当公众号发布新文章时,可以把消息写入用户的消息列表中。
例如用户 1001 订阅了某个公众号,公众号发布文章后,给用户推送一条消息:
LPUSH user:1001:subscribe:messages article:3001
用户进入订阅消息页面时,查询最近的订阅内容:
LRANGE user:1001:subscribe:messages 0 9
如果有多个用户订阅同一个公众号,可以在发布文章时,把文章 ID 分别写入不同用户的消息列表中。
例如:
LPUSH user:1001:subscribe:messages article:3001
LPUSH user:1002:subscribe:messages article:3001
LPUSH user:1003:subscribe:messages article:3001
这种方式实现简单,适合小规模消息推送场景。
但是如果订阅用户数量非常大,逐个用户写入 List 会造成较大的写入压力,这时就需要考虑更专业的消息系统或异步推送方案。
4.2.6 任务排队处理
List 还可以作为简单的任务队列使用。
例如系统中有一些任务需要异步处理,可以先把任务放入 List 中:
LPUSH job:queue job:1001
LPUSH job:queue job:1002
LPUSH job:queue job:1003
后台程序从队列中取出任务并执行:
RPOP job:queue
如果希望后台程序一直等待任务,可以使用阻塞式命令:
BRPOP job:queue 0
这种方式适合简单的异步任务处理,比如:
- 发送短信
- 发送邮件
- 生成报表
- 处理日志
- 执行简单后台任务
4.2.7 使用 List 做队列的注意事项
List 可以实现简单队列,但它并不是完整的消息队列系统。
如果业务只需要简单的生产和消费,List 是可以满足需求的。
但是如果需要下面这些能力,就不建议只使用 List:
- 消费组
- ACK 确认机制
- 失败重试
- 消息追踪
- 消息持久化保障
- 多消费者协同消费
- 死信队列
- 消费进度管理
这种情况下,应该优先考虑 Redis Stream,或者使用专业 MQ,例如 RabbitMQ、RocketMQ、Kafka 等。
简单来说:
简单队列:可以用 Redis List
复杂消息队列:优先看 Redis Stream 或专业 MQ
5. Hash 哈希
Hash 是一个 key 下面挂多个 field-value。它很适合存对象的部分字段。
例如用户对象:
hset user:1001 name zhangsan age 18 city beijing
hget user:1001 name
hgetall user:1001
5.1 常用命令
| 命令 | 说明 |
|---|---|
| HSET key field value [field value …] | 设置一个或多个字段 |
| HGET key field | 获取字段 |
| HMGET key field [field …] | 批量获取字段 |
| HGETALL key | 获取所有字段和值 |
| HDEL key field [field …] | 删除字段 |
| HLEN key | 字段数量 |
| HEXISTS key field | 判断字段是否存在 |
| HKEYS key | 获取所有 field |
| HVALS key | 获取所有 value |
| HINCRBY key field increment | 整数字段自增 |
| HINCRBYFLOAT key field increment | 浮点字段自增 |
| HSETNX key field value | 字段不存在时写入 |
HMSET 在老资料里经常出现,新版本直接用 HSET 设置多个 field-value 即可。
HSETNX 对单个 field 的判断和写入是原子的,因为 Redis 单条命令在服务端串行执行。但如果业务要同时判断多个 field、检查外部条件再写入,就需要 Lua 脚本或事务把逻辑收敛到 Redis 侧,避免应用侧读写之间出现竞态。
5.2 使用场景
Redis 的 Hash 适合用来存储对象类型的数据。
Hash 的结构类似于 Java 中的 Map,一个 Redis key 下面可以保存多个 field 和 value。
从内部编码看,小 Hash 可能使用 listpack 等紧凑结构,规模变大后会转成 hashtable。写文章或排查 Redis 7 时,不建议再用 ziplist 作为主术语。
可以简单理解为:
key -> field -> value
例如:
user:1001 -> name -> zhangsan
user:1001 -> age -> 18
user:1001 -> city -> Beijing
Hash 常见使用场景:
- 用户信息
- 商品信息
- 购物车
- 系统配置
5.2.1 用户信息
Hash 非常适合保存用户对象。
例如保存用户 1001 的基本信息:
HSET user:1001 name zhangsan age 18 gender male city Beijing
查看用户的某个字段:
HGET user:1001 name
查看用户所有信息:
HGETALL user:1001
修改用户某个字段:
HSET user:1001 city Shanghai
这种方式的好处是,如果只需要修改用户的某一个字段,不需要把整个用户对象取出来再重新写入,只需要更新指定 field 即可。
5.2.2 商品信息
商品详情也可以使用 Hash 存储。
例如保存商品 2001 的信息:
HSET product:2001 name "iPhone 15" price 5999 stock 100 category phone
获取商品价格:
HGET product:2001 price
修改商品库存:
HSET product:2001 stock 99
查看商品完整信息:
HGETALL product:2001
对于商品这种字段较多、并且经常需要读取或修改局部字段的数据,Hash 使用起来比较方便。
5.2.3 购物车
购物车也是 Hash 的典型使用场景。
可以把用户的购物车作为一个 Hash,其中 field 表示商品 ID,value 表示商品数量。
例如用户 1001 的购物车:
HSET cart:1001 product:2001 1
HSET cart:1001 product:2002 3
HSET cart:1001 product:2003 2
查看某个商品的购买数量:
HGET cart:1001 product:2002
修改商品数量:
HSET cart:1001 product:2002 5
删除购物车中的某个商品:
HDEL cart:1001 product:2003
查看整个购物车:
HGETALL cart:1001
这种设计比较直观:
cart:用户ID -> 商品ID -> 商品数量
5.2.4 系统配置
Hash 也可以用于保存一些系统配置项。
例如保存网站配置:
HSET system:config site_name "My Blog" theme dark page_size 20
获取某个配置:
HGET system:config site_name
修改配置:
HSET system:config page_size 30
查看所有配置:
HGETALL system:config
这种方式适合保存一组相关配置,避免为每个配置项单独创建一个 Redis key。
5.2.5 Hash 和 String 存 JSON 的区别
在 Redis 中,对象数据既可以用 Hash 保存,也可以用 String 保存 JSON 字符串。
例如使用 String 保存整个用户对象:
SET user:1001 '{"name":"zhangsan","age":18,"city":"Beijing"}'
这种方式比较简单,适合整体读写。
但是如果只想修改其中一个字段,比如只修改 city,就需要先读取整个 JSON,反序列化后修改字段,再重新写回 Redis。
而使用 Hash 时,可以直接修改某一个字段:
HSET user:1001 city Shanghai
所以从本质上看:
Hash 更适合频繁读写对象的局部字段;
String 更适合保存整体对象 JSON。
如果业务中经常需要单独修改对象的某些字段,例如用户昵称、商品库存、购物车数量等,使用 Hash 会更方便。
如果业务中每次都是读取完整对象,或者直接把对象整体缓存起来,那么使用 String 保存 JSON 反而更加简单。
5.2.6 使用 Hash 的注意事项
Hash 虽然适合存储对象,但也不是所有对象都必须使用 Hash。
使用时可以参考下面的原则:
- 字段较多,并且经常单独读写某些字段,可以使用 Hash;
- 数据经常整体读写,不需要单独修改字段,可以使用 String + JSON;
- 对象结构比较复杂,包含多层嵌套时,String + JSON 通常更直观;
- Hash 适合扁平对象,不太适合保存特别复杂的嵌套对象;
- 如果 field 数量过多,也需要注意 Redis 内存占用和 key 的设计。
简单总结:
局部字段读写:优先考虑 Hash
整体对象缓存:优先考虑 String + JSON
6. Set 集合
Set 是无序不重复集合,适合做去重和集合运算。
6.1 常用命令
| 命令 | 说明 |
|---|---|
| SADD key member [member …] | 添加元素 |
| SMEMBERS key | 获取所有元素 |
| SISMEMBER key member | 判断元素是否存在 |
| SREM key member [member …] | 删除元素 |
| SCARD key | 元素数量 |
| SRANDMEMBER key [count] | 随机返回元素,不删除 |
| SPOP key [count] | 随机弹出元素,会删除 |
| SMOVE source destination member | 移动元素 |
| SINTER key [key …] | 交集 |
| SUNION key [key …] | 并集 |
| SDIFF key [key …] | 差集 |
| SINTERCARD numkeys key [key …] [LIMIT limit] | 返回交集数量,Redis 7 可用 |
示例:
sadd post:1:likes u1 u2 u3
sadd post:2:likes u2 u3 u4
sinter post:1:likes post:2:likes
scard post:1:likes
集合运算示意:
127.0.0.1:6379> sadd set1 a b c 1 2
(integer) 5
127.0.0.1:6379> sadd set2 1 2 3 a x
(integer) 5
127.0.0.1:6379> sdiff set1 set2
1) "b"
2) "c"
127.0.0.1:6379> sunion set1 set2
1) "x"
2) "a"
3) "3"
4) "1"
5) "2"
6) "b"
7) "c"
127.0.0.1:6379> sinter set1 set2
1) "a"
2) "1"
3) "2"
127.0.0.1:6379> sintercard 2 set1 set2
(integer) 3
6.2 使用场景
Redis 的 Set 是一个无序集合,集合中的元素不能重复。
Set 最大的特点就是:
无序、唯一、支持集合运算
所以它非常适合用来处理去重、判断是否存在、随机抽取、共同关系计算等场景。
Set 常见场景:
- 抽奖池,使用 SPOP 随机抽出并删除
- 点赞用户集合,使用 SISMEMBER 判断是否已点赞
- 共同关注,使用 SINTER 求交集
- 标签去重,天然不重复
6.2.1 抽奖池
Set 可以用来实现简单的抽奖池。
因为 Set 中的元素不会重复,所以可以避免同一个用户被重复加入抽奖池。
例如把参与抽奖的用户 ID 加入集合:
SADD lottery:users 1001 1002 1003 1004 1005
随机抽出一个用户,并从集合中删除:
SPOP lottery:users
如果一次抽出多个用户:
SPOP lottery:users 3
SPOP 会随机返回集合中的元素,并且会把这些元素从集合中删除,所以非常适合“不允许重复中奖”的抽奖场景。
这也意味着 SPOP 是消费型随机抽取,被抽出的元素会从集合中移除。如果抽奖结果需要可恢复或可审计,应该先记录中奖结果,再考虑是否从池子里删除。
如果只是随机查看用户,但不想删除,可以使用:
SRANDMEMBER lottery:users
如果随机查看多个用户:
SRANDMEMBER lottery:users 3
简单来说:
SPOP:随机取出并删除
SRANDMEMBER:随机取出但不删除
6.2.2 点赞用户集合
Set 也很适合用来保存点赞用户列表。
比如文章 1001 被哪些用户点赞,可以使用一个 Set 来保存:
SADD article:1001:likes user:1
SADD article:1001:likes user:2
SADD article:1001:likes user:3
判断某个用户是否已经点赞:
SISMEMBER article:1001:likes user:1
如果返回 1,表示已经点赞;如果返回 0,表示没有点赞。
取消点赞时,可以从集合中移除该用户:
SREM article:1001:likes user:1
统计文章点赞数:
SCARD article:1001:likes
这种方式的好处是,Set 天然去重,同一个用户即使重复执行点赞操作,也不会在集合中出现多次。
6.2.3 共同关注
Set 的交集运算非常适合用来计算共同关注、共同好友、共同标签等关系。
比如用户 1001 关注的人:
SADD user:1001:follows user:2001 user:2002 user:2003
用户 1002 关注的人:
SADD user:1002:follows user:2002 user:2003 user:2004
计算两个用户的共同关注:
SINTER user:1001:follows user:1002:follows
返回结果可能是:
user:2002
user:2003
这表示用户 1001 和用户 1002 都关注了这两个用户。
除了交集之外,Set 还支持并集和差集:
SUNION user:1001:follows user:1002:follows
表示两个用户关注对象的并集。
SDIFF user:1001:follows user:1002:follows
表示用户 1001 关注了,但用户 1002 没有关注的人。
6.2.4 标签去重
Set 还可以用来做标签去重。
比如一篇文章有多个标签:
SADD article:1001:tags Redis Java Backend Redis
因为 Set 中的元素不能重复,所以即使重复添加 Redis,集合中也只会保存一份。
查看文章的所有标签:
SMEMBERS article:1001:tags
给文章删除某个标签:
SREM article:1001:tags Java
统计文章标签数量:
SCARD article:1001:tags
这种方式适合保存文章标签、商品标签、用户兴趣标签等数据。
6.2.5 三个典型场景总结
Set 最典型的三个使用场景可以总结为:
| 场景 | 常用命令 | 说明 |
|---|---|---|
| 抽奖池 | SADD、SPOP、SRANDMEMBER | 随机抽取用户,可选择是否删除 |
| 点赞集合 | SADD、SREM、SISMEMBER、SCARD | 保存点赞用户,判断是否已点赞,统计点赞数 |
| 共同关注 | SINTER、SUNION、SDIFF | 计算交集、并集、差集 |
6.2.6 使用 Set 的注意事项
Set 非常适合处理去重和集合运算,但也有一些需要注意的地方。
首先,Set 是无序的,不能按照添加顺序取出元素。如果业务需要按照时间排序,例如最新点赞列表、最新评论列表,更适合使用 List 或 ZSet。
其次,Set 适合判断某个元素是否存在,例如判断用户是否点赞、是否收藏、是否加入某个分组。
最后,如果业务需要按照分数排序,例如排行榜、热度榜、权重排序,就应该使用 ZSet,而不是 Set。
集合运算也要控制规模。SINTER、SUNION、SDIFF 在大集合上会占用 Redis 主线程较长时间,容易影响线上请求。大集合交并差可以考虑离线计算、分批计算,或者用 SINTERCARD 这类只返回数量的命令降低开销。
简单总结:
只需要去重和判断存在:使用 Set
需要排序:使用 ZSet
需要按插入顺序保存:使用 List
7. ZSet 有序集合
ZSet 是带分数的 Set。元素不重复,但每个元素都有一个 score,Redis 会根据 score 排序。score 相同时,Redis 会按 member 的字典序作为补充排序规则,这一点在排行榜并列名次时要提前设计清楚。
7.1 常用命令
| 命令 | 说明 |
|---|---|
| ZADD key score member [score member …] | 添加元素 |
| ZRANGE key start stop [WITHSCORES] | 按排名范围正序获取 |
| ZREVRANGE key start stop [WITHSCORES] | 按排名范围倒序获取 |
| ZRANGE key min max BYSCORE [WITHSCORES] | 按分数范围获取,新写法 |
| ZSCORE key member | 获取分数 |
| ZCARD key | 元素数量 |
| ZREM key member [member …] | 删除元素 |
| ZINCRBY key increment member | 增加元素分数 |
| ZCOUNT key min max | 指定分数范围内元素数量 |
| ZRANK key member | 正序排名,从 0 开始 |
| ZREVRANK key member | 倒序排名,从 0 开始 |
| ZMPOP | 从一个或多个有序集合中弹出元素 |
示例:
zadd hot:article 100 article:1 80 article:2 120 article:3
zrevrange hot:article 0 9 withscores
zincrby hot:article 1 article:2
zrevrank hot:article article:2
ZMPOP 演示图:
127.0.0.1:6379> zmpop 1 zset1 min count 1
1) "zset1"
2) 1) 1) "v3"
2) "80"
127.0.0.1:6379> zrange zset1 0 -1 withscores
1) "v4"
2) "100"
3) "v5"
4) "110"
5) "v2"
6) "170"
7.2 使用场景
ZSet 最典型的场景是排行榜:
根据商品销售对商品进行排序显示
思路:定义商品销售排行榜(sorted set 集合),key 为 goods:sellsort,分数为商品销售数量。
| 说明 | 命令 |
|---|---|
| 商品编号 1001 的销量是 9,商品编号 1002 的销量是 15 | ZADD goods:sellsort 9 1001 15 1002 |
| 有一个客户又买了 2 件商品 1001,商品编号 1001 销量加 2 | ZINCRBY goods:sellsort 2 1001 |
| 求商品销量前 10 名 | ZREVRANGE goods:sellsort 0 9 WITHSCORES |
常见设计是:
127.0.0.1:6379> ZADD goods:sellsort 9 1001 15 1002
(integer) 2
127.0.0.1:6379> ZINCRBY goods:sellsort 2 1001
"11"
127.0.0.1:6379> ZREVRANGE goods:sellsort 0 9 WITHSCORES
1) "1002"
2) "15"
3) "1001"
4) "11"
8. Bitmap 位图

Bitmap 并不是 Redis 中一种全新的数据类型,它本质上还是基于 String 实现的。
Redis 的 String 是二进制安全的,可以直接操作二进制位。Bitmap 就是把一个字符串看成一串连续的二进制位,然后通过位操作来记录某种状态。
简单理解:
一个 bit 位只能表示两种状态:0 或 1
所以 Bitmap 非常适合用来记录只有两种状态的数据,例如:
- 是否签到
- 是否登录
- 是否点赞
- 是否在线
- 是否打卡
- 是否完成某个任务
8.1 Bitmap 的基本思想
假设要记录一个用户一年 365 天的签到情况,可以把一年中的每一天对应到 Bitmap 中的一个 bit 位。
例如:
第 1 天 -> offset 0
第 2 天 -> offset 1
第 3 天 -> offset 2
...
第 365 天 -> offset 364
如果某一天签到了,就把对应位置设置为 1;如果没有签到,就保持为 0。
例如:
SETBIT sign:1001 0 1
SETBIT sign:1001 1 1
SETBIT sign:1001 364 1
表示用户 1001 在第 1 天、第 2 天、第 365 天都签到了。
8.2 Bitmap 为什么省内存
Bitmap 的核心优势是省内存。
因为它使用的是 bit 位,而不是普通字符串、数字或者对象。
一个 bit 位只占:
1 bit
而 1 个字节等于:
8 bit
所以如果按年记录一个用户的签到情况,365 天只需要:
365 / 8 ≈ 46 Byte
也就是说,一个用户一整年的签到记录,大约只需要 46 字节。
如果有 1000 万用户,一年签到记录大约需要:
46 Byte * 1000W ≈ 460 MB
如果是按天统计亿级用户的某种状态,比如每天记录 1 亿用户是否登录,那么每天需要:
1 亿 bit = 100000000 / 8 / 1024 / 1024 ≈ 12 MB
10 天大约需要:
120 MB
所以 Bitmap 非常适合大量布尔状态统计。
8.3 Bitmap 常用命令
设置某个位置的 bit 值:
SETBIT key offset value
例如设置用户第 1 天签到:
SETBIT sign:1001 0 1
获取某个位置的 bit 值:
GETBIT key offset
例如查看用户第 1 天是否签到:
GETBIT sign:1001 0
统计 Bitmap 中值为 1 的数量:
BITCOUNT key
例如统计用户一年签到多少天:
BITCOUNT sign:1001
查看 Bitmap 实际占用的字符串长度:
STRLEN key
例如:
STRLEN sign:1001
8.4 Bitmap 的典型使用场景
Bitmap 适合存储大量二值状态数据。
常见场景包括:
- 用户签到统计
- 用户在线状态统计
- 用户每日登录统计
- 用户是否点赞
- 用户是否完成任务
- 活跃用户统计
- 布尔状态压缩存储
例如记录用户是否签到:
0 表示未签到
1 表示已签到
记录用户是否在线:
0 表示不在线
1 表示在线
记录某一天哪些用户登录过:
offset 可以使用用户 ID
value 为 1 表示该用户登录过
8.5 Bitmap 的优点和缺点
Bitmap 的优点是非常省内存,适合大量布尔状态统计。
比如用普通方式存储 1000 万用户的签到状态,可能需要大量 key 或大量字符串;而使用 Bitmap,只需要一段连续的 bit 位即可表示。
但是 Bitmap 也有缺点。
首先,offset 的设计需要提前规划清楚。例如使用用户 ID 作为 offset 时,如果用户 ID 很稀疏,可能会造成空间浪费。
这里的浪费来自最大 offset。Bitmap 会按最高位扩展底层字符串,如果直接使用很大的、不连续的用户 ID 作为 offset,即使实际只有少量用户,也可能占用很大的字符串空间。因此 Bitmap 更适合连续编号、可映射编号或按日期分片后的密集状态,不适合动态且稀疏的 ID 空间。
其次,Bitmap 更适合表示简单的 0 / 1 状态,不适合存储复杂信息。
另外,如果后期业务规则发生变化,offset 的含义需要调整,迁移成本可能会比较高。
8.6 使用 Bitmap 的注意事项
使用 Bitmap 时,最重要的是提前设计好 offset 的含义。
例如:
按天签到:offset 表示一年中的第几天
用户登录:offset 表示用户 ID
任务完成:offset 表示任务编号
如果 offset 设计合理,Bitmap 可以非常节省内存。
如果 offset 设计不合理,例如 offset 很大但实际数据很少,就可能造成空间浪费。
此外,在实际使用时,最好给 Bitmap 设置过期时间,让 Redis 自动删除不再需要的历史数据,从而节省内存。
例如按天记录登录用户:
EXPIRE login:2024-09-20 2592000
表示这一天的登录记录保存 30 天后自动过期。
8.7 简单总结
Bitmap 的核心特点可以总结为:
用一个 bit 位表示一个状态
它适合处理大量 0 / 1 状态数据,例如签到、登录、在线、点赞等。
简单来说:
大量布尔状态统计:适合使用 Bitmap
需要保存复杂对象信息:不适合使用 Bitmap
Bitmap 的优势是省内存,适合大量布尔状态统计。缺点是 offset 设计要提前想清楚,否则后期迁移成本很高。
9. HyperLogLog 基数统计
HyperLogLog 是 Redis 提供的一种用于基数统计的数据结构。
所谓基数统计,就是统计去重后的数量。例如统计网站 UV 时,同一个用户访问多次,只需要计算为 1 个独立用户,这种场景就可以使用 HyperLogLog。
Redis 在 2.8.9 版本中添加了 HyperLogLog 结构。
HyperLogLog 的优点是:当输入元素的数量非常大,或者数据体积非常大时,计算基数所需要的空间是固定的,并且非常小。
在 Redis 中,每个 HyperLogLog key 只需要大约 12KB 内存,就可以统计接近 2^64 个不同元素的基数。
这和使用 Set 做去重统计形成了明显对比:
使用 Set 可以保存每一个具体元素,但是元素越多,占用内存越大;
使用 HyperLogLog 只统计去重后的数量,不保存具体元素,所以内存占用非常小。
需要注意的是,HyperLogLog 只会根据输入元素计算基数,并不会保存输入元素本身。
因此,HyperLogLog 适合用于统计数量,例如:
- 网站 UV 统计
- 页面访问用户数统计
- 搜索关键词去重统计
- 活动参与人数统计
- 大规模用户去重计数
但是它不适合用于保存明细数据。
如果业务需要知道具体有哪些用户访问过页面,应该使用 Set 或其他数据结构;如果只关心去重后的数量,就可以使用 HyperLogLog。
9.1 常用命令
| 命令 | 说明 |
|---|---|
| PFADD key element [element …] | 添加元素 |
| PFCOUNT key [key …] | 返回基数估算值 |
| PFMERGE destkey sourcekey [sourcekey …] | 合并多个 HyperLogLog |
示例:
pfadd uv:20240920 u1 u2 u3 u1
pfcount uv:20240920
pfmerge uv:202409 uv:20240901 uv:20240902 uv:20240920
命令演示图:
127.0.0.1:6379> pfadd hll01 1 3 4 5 7 9
(integer) 0
127.0.0.1:6379> pfadd hll02 2 4 4 4 6 8 9
(integer) 1
127.0.0.1:6379> pfcount hll02
(integer) 5
127.0.0.1:6379> pfmerge distResult hll01 hll02
OK
127.0.0.1:6379> pfcount distResult
(integer) 9
需要注意:HyperLogLog 是近似统计,标准误差大约是 0.81%。它不返回具体用户列表,也不能用于严格计数、回溯明细或判断某个元素是否出现过。
10. GEO 地理空间
GEO 是 Redis 提供的地理位置功能,主要用于存储经纬度信息,并根据距离查询附近的位置。从实现上看,GEO 底层使用 geohash 编码并存储在有序集合中,距离计算使用球面距离近似算法,适合附近查询,不适合替代专业地图路线规划。
在移动互联网应用中,经常会遇到基于位置查询的场景,例如:
- 交友软件中查找附近的人;
- 外卖软件中查找附近的店铺;
- 地图软件中查找附近的地点;
- 打车软件中查找附近的车辆。
地球上的位置通常使用经纬度表示:
经度范围:-180 到 180
纬度范围:-90 到 90
只要确定一个位置的经纬度,就可以确定它在地球上的大致位置。
如果使用普通数据库查询附近车辆,可能会写出类似下面的 SQL:
select taxi
from position
where x0 - r < x < x0 + r
and y0 - r < y < y0 + r;
这种方式看起来可以查询附近范围内的数据,但会存在一些问题:
- 数据量大、并发高时,查询性能压力较大;
- 查询出来的是一个矩形范围,而不是以当前位置为中心的圆形范围;
- 地球不是平面,而是球体,简单使用经纬度范围计算距离会有一定误差。
Redis GEO 就是为这类场景提供支持的。它可以帮助我们完成添加地理位置、获取坐标、计算距离、查询附近位置等操作。
简单来说,GEO 适合处理“附近的人”“附近门店”“附近车辆”等基于地理位置的查询场景。
10.1 常用命令
| 命令 | 说明 |
|---|---|
| GEOADD key longitude latitude member | 添加位置 |
| GEOPOS key member [member …] | 查看经纬度 |
| GEODIST key member1 member2 [unit] | 计算距离 |
| GEOHASH key member [member …] | 返回 geohash |
| GEOSEARCH key … | 按成员或坐标搜索附近位置 |
老资料里常见 GEORADIUS、GEORADIUSBYMEMBER,Redis 新版本更推荐使用 GEOSEARCH。
示例:
geoadd city:shop 116.397128 39.916527 shop:beijing
geoadd city:shop 121.473701 31.230416 shop:shanghai
geopos city:shop shop:beijing
geodist city:shop shop:beijing shop:shanghai km
geosearch city:shop frommember shop:beijing byradius 10 km withdist
10.2 GEO 半径查询
Redis GEO 可以根据指定的经纬度坐标,查询一定半径范围内的位置元素。
例如以某个经纬度为中心,查询 10 公里范围内的位置:
GEOSEARCH city FROMLONLAT 116.418017 39.914402 BYRADIUS 10 km WITHDIST WITHCOORD COUNT 10
含义如下:
GEOSEARCH city FROMLONLAT 116.418017 39.914402 BYRADIUS 10 km
表示以经度 116.418017、纬度 39.914402 为中心,查询 city 这个 GEO 集合中距离中心点不超过 10 km 的位置元素。
常用参数说明:
- WITHDIST:返回位置元素的同时,也返回该位置与中心点之间的距离。
- WITHCOORD:返回位置元素的经度和纬度。
- WITHHASH:返回位置元素经过 geohash 编码后的有序集合分值,主要用于底层调试,实际业务中使用较少。
- COUNT:限制返回的记录数量。
例如:
GEOSEARCH city FROMLONLAT 116.418017 39.914402 BYRADIUS 10 km WITHDIST WITHCOORD COUNT 10
表示查询当前位置附近 10 公里内的地点,并返回距离、经纬度,最多返回 10 条记录。
需要注意的是,116.418017 39.914402 表示当前所在位置的经纬度。在实际业务中,这个坐标通常来自用户手机定位、地图定位或者前端传入的位置数据。
GEO 常用于附近门店、附近的人、附近车辆、同城服务等场景。如果需要复杂路径规划、导航路线、道路距离计算,通常还需要交给专业地图服务处理。
11. Stream 流
Stream 是 Redis 5 引入的日志型消息数据结构,可以理解为 Redis 自带的消息流。
它支持:
- 自动生成消息 ID
- 按 ID 范围读取
- 阻塞读取
- 消费组
- ACK 确认
- pending 消息查看

在 Redis Stream 中,消费组相关的几个核心概念如下:
| 名称 | 作用 |
|---|---|
| Message Content | 消息内容,保存真正的业务数据。 |
| Consumer Group | 消费组,用于让多个消费者共同消费一个 Stream。 |
| Last_delivered_id | 消费组游标,记录当前消费组读取到的位置。 |
| Consumer | 消费者,消费组中的具体消费实例。 |
| Pending_ids / PEL | 待确认消息列表,记录已经被消费者读取但还没有 ACK 的消息。 |
其中最重要的是 PEL。
消费者通过 XREADGROUP 读取消息后,消息不会立刻从 Stream 中删除,而是会进入当前消费者的 PEL 中。
只有当消费者处理完成,并执行 XACK streamKey groupName messageId 后,这条消息才会从待确认列表中移除。注意,XACK 只是把消息从 PEL 中标记为已确认,并不会删除 Stream 里的消息本体;是否删除或裁剪,要通过 XDEL、XTRIM 或保留策略单独处理。
11.1 队列相关命令
| 命令 | 说明 |
|---|---|
| XADD key * field value [field value …] | 添加消息 |
| XLEN key | 消息长度 |
| XRANGE key start end | 正序范围读取 |
| XREVRANGE key end start | 倒序范围读取 |
| XREAD [BLOCK ms] STREAMS key id | 读取消息,可阻塞 |
| XDEL key id [id …] | 删除消息 |
| XTRIM key MAXLEN … | 裁剪 Stream 长度 |
xadd order:stream * orderId 1001 amount 99.9
xadd order:stream * orderId 1002 amount 188.0
xlen order:stream
xrange order:stream - +
xread count 2 streams order:stream 0
11.2 消费组命令
| 命令 | 说明 |
|---|---|
| XGROUP CREATE key group id [MKSTREAM] | 创建消费组 |
| XREADGROUP GROUP group consumer STREAMS key id | 消费组读取 |
| XACK key group id [id …] | 确认消息 |
| XPENDING key group | 查看待确认消息 |
| XINFO STREAM key | 查看 Stream 信息 |
| XINFO GROUPS key | 查看消费组信息 |
示例:
xgroup create order:stream g1 0 mkstream
xreadgroup group g1 c1 count 1 streams order:stream >
xpending order:stream g1
xack order:stream g1 1690000000000-0
下面是消费组和 ACK 的命令示例:
# 用于创建消费者组
127.0.0.1:6379> XGROUP CREATE mystream groupA $
OK
127.0.0.1:6379> XGROUP CREATE mystream groupB 0
OK
127.0.0.1:6379>
# `$` 表示从 Stream 尾部开始消费,只消费创建消费者组之后产生的新消息。
# `0` 表示从 Stream 头部开始消费,也就是从第一条消息开始消费。
# 创建消费者组的时候必须指定 ID:
# - ID 为 `0`:表示从头开始消费
# - ID 为 `$`:表示只消费新的消息,队尾新来
127.0.0.1:6379> XPENDING mystream groupA # 查询 mystream 中 groupA 消费组内所有“已读取但尚未确认”的消息
1) (integer) 3 # 当前消费组中一共有 3 条待确认消息
2) "1659430290156-0" # 所有消费者读取的待确认消息中,最小的消息 ID
3) "1659430750387-0" # 所有消费者读取的待确认消息中,最大的消息 ID
4) 1) 1) "consumer1" # 消费者 consumer1
2) "3" # consumer1 一共读取了 3 条待确认消息
127.0.0.1:6379> XPENDING mystream groupB # 查询 mystream 中 groupB 消费组内所有“已读取但尚未确认”的消息
1) (integer) 3 # 当前消费组中一共有 3 条待确认消息
2) "1659430290156-0" # 待确认消息中最小的消息 ID
3) "1659430750387-0" # 待确认消息中最大的消息 ID
4) 1) 1) "consumer1" # 消费者 consumer1
2) "3" # consumer1 一口气读取了 3 条待确认消息
127.0.0.1:6379> XPENDING mystream groupC # 查询 mystream 中 groupC 消费组内所有“已读取但尚未确认”的消息
1) (integer) 3 # 当前消费组中一共有 3 条待确认消息
2) "1659430290156-0" # 待确认消息中最小的消息 ID
3) "1659430750387-0" # 待确认消息中最大的消息 ID
4) 1) 1) "consumer1" # 第一个消费者 consumer1
2) "1" # consumer1 读取了 1 条待确认消息
2) 1) "consumer2" # 第二个消费者 consumer2
2) "1" # consumer2 读取了 1 条待确认消息
3) 1) "consumer3" # 第三个消费者 consumer3
2) "1" # consumer3 读取了 1 条待确认消息
127.0.0.1:6379> # 等待继续输入命令
127.0.0.1:6379> XPENDING mystream groupD - + 10 consumer1 # 查询 mystream 中 groupD 消费组里 consumer1 的待确认消息,最多返回 10 条
1) 1) "1659430290156-0" # 第一条待确认消息的 ID
2) "consumer1" # 该消息属于 consumer1
3) (integer) 370524 # 消息空闲时间,表示距离上次投递过去的毫秒数
4) (integer) 1 # 消息被投递的次数
2) 1) "1659430293537-0" # 第二条待确认消息的 ID
2) "consumer1" # 该消息属于 consumer1
3) (integer) 139698 # 消息空闲时间
4) (integer) 1 # 消息被投递的次数
127.0.0.1:6379> XACK mystream groupD 1659430293537-0 # 确认 groupD 中 ID 为 1659430293537-0 的消息已经处理完成
(integer) 1 # 成功确认 1 条消息
127.0.0.1:6379> XPENDING mystream groupD - + 10 consumer1 # 再次查询 consumer1 的待确认消息
1) 1) "1659430290156-0" # ACK 之后只剩下这一条待确认消息
2) "consumer1" # 该消息属于 consumer1
3) (integer) 395434 # 消息空闲时间
4) (integer) 1 # 消息被投递的次数
127.0.0.1:6379> # 等待继续输入命令
Stream 适合轻量消息流,但不能简单等价替代 Kafka、RabbitMQ。它没有 Kafka 那样的 log compaction 语义,也不适合作为大规模跨机房、高吞吐、强可观测的消息平台。生产上如果对消息堆积、重平衡、跨机房、高吞吐和可观测要求很高,仍然应该优先考虑专业 MQ。
12. Bitfield 位域
Bitfield 可以把一个字符串看成连续二进制位,并按指定 bit 宽读写整数。
BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组,并对这个数组中任意偏移进行访问。
可以使用该命令对一个有符号的 5 位整型数的第 1234 位设置指定值,也可以对一个 31 位无符号整型数的第 4567 位进行取值。
类似地,本命令可以对指定的整数进行自增和自减操作,并且可以配置上溢和下溢处理操作。
BITFIELD 命令可以在一次调用中同时对多个位范围进行操作:它接受一系列待执行的操作作为参数,并返回一个数组,数组中的每个元素就是对应操作的执行结果。
常用形式:
bitfield key set u8 0 10
bitfield key get u8 0
bitfield key incrby u8 0 1
参数中 u8 表示无符号 8 位整数,offset 表示从哪一位开始读写。
溢出控制可以使用:
bitfield key overflow sat incrby u8 0 300
溢出策略主要有三种:WRAP 表示按位宽回绕,SAT 表示饱和到最大或最小值,FAIL 表示溢出时返回 nil 并不执行该操作。
Bitfield 的优点是极致节省空间,缺点是编码复杂,业务可读性很差。除非是对内存非常敏感、字段含义稳定且有明确文档的状态编码,否则普通业务不建议第一优先级使用它。
13. 数据类型选择建议
最后放一张选择表,实际开发时按它判断就够了。
| 需求 | 优先考虑 |
|---|---|
| 单值缓存、验证码、计数器 | String |
| 对象有多个字段且要局部更新 | Hash |
| 最新列表、简单队列 | List |
| 去重、标签、共同好友 | Set |
| 排行榜、按分数排序 | ZSet |
| 签到、布尔状态统计 | Bitmap |
| UV 估算,不要求精确 | HyperLogLog |
| 附近位置查询 | GEO |
| 轻量消息流、消费组 | Stream |
| 极致压缩状态字段 | Bitfield |
几个容易混淆的选择可以单独看:
| 对比 | 选择建议 |
|---|---|
| List vs ZSet vs Stream | 只要插入顺序用 List;需要按分数排序用 ZSet;需要消费组、ACK 和 pending 跟踪用 Stream |
| Bitmap vs Set | ID 连续、状态密集、只需要 0/1 统计用 Bitmap;ID 稀疏、需要保存成员明细或遍历成员用 Set |
| ZSet vs List | 排行榜、延迟任务、按权重查询用 ZSet;最新列表和简单队列用 List |
有一个简单判断方法:如果你只是在缓存一段 JSON,用 String;如果你需要频繁操作集合关系、排行、计数、状态位,就应该让 Redis 的数据结构直接表达业务,而不是把所有内容都塞到 JSON 字符串里。
14. 本文小结
Redis 的数据类型不是为了“多记几个命令”,而是为了让应用少写很多重复的状态管理逻辑。
本文最常用的命令可以浓缩成下面这些:
# key
exists key
type key
expire key 60
ttl key
unlink key
# string
set k v
get k
incr counter
set lock:order:1 uuid nx ex 30
# list
lpush list a b c
lrange list 0 -1
rpop list
# hash
hset user:1 name zhangsan age 18
hgetall user:1
# set
sadd tags redis mysql kafka
sinter set1 set2
# zset
zadd rank 100 user1
zrevrange rank 0 9 withscores
# bitmap
setbit sign:user:1 1 1
bitcount sign:user:1
# hyperloglog
pfadd uv:day u1 u2 u3
pfcount uv:day
# geo
geoadd shops 116.397128 39.916527 shop:1
geosearch shops frommember shop:1 byradius 3 km
# stream
xadd order:stream * orderId 1001
xread streams order:stream 0