redis常见数据类型
基本全局命令
| 命令 | 语法 | 时间复杂度 | 返回值 | 说明 | 实例 |
|---|---|---|---|---|---|
| keys | keys pattern |
O(N) | 匹配 pattern 的所有 key | 返回所有满足样式(pattern)的 key。支持如下通配样式: • h?llo匹配 hello,hallo 和 hxllo • h*llo匹配 hllo 和 heeeello • h[ae]llo匹配 hello 和 hallo 但不匹配 hillo • h[^e]llo匹配 hallo,hbllo,... 但不匹配 hello • h[a-b]llo匹配 hallo 和 hbllo |
keys user:*查找所有以"user:"开头的key |
| exists | exists key [key ...] |
O(1) 对每个 key | 存在的 key 的数量 | 检查给定的 key 是否存在。可以同时检查多个 key,返回存在的 key 数量。 | exists name age检查name和age两个key是否存在 |
| del | del key [key ...] |
O(1) 对每个 key | 被删除的 key 的数量 | 删除给定的一个或多个 key。如果 key 不存在,则忽略。 | del key1 key2删除key1和key2 |
| expire | expire key seconds |
O(1) | 1 表示设置成功,0 表示 key 不存在 | 为给定的 key 设置过期时间(以秒为单位)。过期后,key 会被自动删除。 | expire session:abc 60设置session:abc 60秒后过期 |
| ttl | ttl key |
O(1) | 剩余的生存时间(以秒为单位) • 返回 -2 表示 key 不存在 • 返回 -1 表示 key 存在但没有设置过期时间 • 返回正数表示剩余的秒数 | 返回给定 key 的剩余生存时间(TTL, time to live)。 | ttl session:abc查看session:abc的剩余生存时间 |
| type | type key |
O(1) | key 所存储的值的类型 可能的返回值: • string(字符串) • list(列表) • set(集合) • zset(有序集合) • hash(哈希) • stream(流) • none(key 不存在) | 返回 key 所存储的值的类型。 | type user:1查看user:1存储的数据类型 |
了解:

一、核心过期策略:两种策略结合
Redis并不是在键过期的时间点立即删除它,而是采用惰性删除和定期删除两种策略相结合的方式,在内存占用、CPU消耗和响应速度之间取得平衡。
1. 惰性删除
策略:当客户端尝试访问一个键时,Redis会先检查这个键是否已过期。如果过期,则立即删除,并返回空值(nil)。如果没过期,则正常返回。
优点:对CPU最友好。删除操作只发生在必须的时候(键被访问时),不会在无关的键上浪费CPU时间。
缺点:对内存不友好。如果一个键永远不再被访问,即使它早已过期,也会一直占用内存,成为"内存垃圾"。这是一种典型的内存泄漏。
2. 定期删除
策略:Redis会周期性地(默认每秒10次,可配置)从设置了过期时间的键集合中,随机抽取一部分键(默认20个,可配置)进行检查,并删除其中过期的键。
这个过程是渐进的、分多次执行的,避免一次性扫描所有键造成服务卡顿。
如果本轮抽查中,过期键的比例超过25%,则立即重复这个过程,直到比例低于25%为止。这是为了应对大量键同时过期的场景。
优点:一种折中方案。通过定期扫描,部分地解决了惰性删除可能造成的内存泄漏问题。
缺点:内存回收不够及时,难以确定最佳的扫描频率和数量。
二、内存淘汰策略
当惰性删除和定期删除仍无法释放足够内存,且新数据需要写入时,Redis会触发内存淘汰机制。这是面试的绝对重点,由配置项 maxmemory-policy 控制。
1. 针对所有键的淘汰策略:
-
noeviction:默认策略。不淘汰任何键,当内存不足时,新写入操作会报错((error) OOM command not allowed when used memory)。适用于不允许数据丢失的场景。
-
allkeys-lru:从所有键中,使用LRU算法(最近最少使用)淘汰最久未访问的键。
-
allkeys-lfu:从所有键中,使用LFU算法(最不经常使用)淘汰访问频率最低的键(Redis 4.0+引入)。
-
allkeys-random:从所有键中,随机淘汰。
2. 针对设置了过期时间的键的淘汰策略:
-
volatile-lru:从设置了过期时间的键中,使用LRU算法淘汰。
-
volatile-lfu:从设置了过期时间的键中,使用LFU算法淘汰。
-
volatile-random:从设置了过期时间的键中,随机淘汰。
-
volatile-ttl:从设置了过期时间的键中,优先淘汰剩余存活时间最短的键。
三、关键面试点与回答思路
面试官可能会这样问:"Redis的键过期了,是怎么删除的?如果内存满了怎么办?"
你可以这样组织回答:
先讲核心删除策略:
"Redis采用惰性删除和定期删除两种策略结合。惰性删除是在访问时检查并删除,节省CPU但可能导致内存泄漏。定期删除是系统每隔一段时间随机抽查一部分过期键并清理,是前者的补充。"
再讲内存淘汰机制:
"如果以上策略没能及时回收内存,导致内存使用达到maxmemory上限,Redis会根据配置的maxmemory-policy进行内存淘汰。"
重点介绍LRU和LFU的区别:
-
LRU:看的是最近一次访问的时间距离现在有多久。它认为"很久没用到的键,将来用到的概率也低"。但可能误伤一个最近被大量访问的历史热点数据。
-
LFU:看的是一段时间内的访问频率。它认为"过去访问次数少的键,将来访问的概率也低"。能更好地保存真正的热点数据。Redis的LFU实现还包含了"访问次数随时间衰减"的机制,防止旧数据长期占据优势。
提一下常用场景:
-
allkeys-lru:最常用,当你的数据访问模式符合幂律分布(二八原则)时效果很好。
-
volatile-ttl:当你希望尽快清除快过期的数据时使用。
-
noeviction:用于必须确保数据不丢失的缓存或数据库场景。
可选:提一下Redis近似LRU/LFU的实现:
"出于性能考虑,Redis使用的是一种近似LRU/LFU算法。它并不是维护一个所有键的精确有序链表(那样成本太高),而是通过随机采样来选择一个最好的键进行淘汰。在Redis 3.0后,通过增加采样数量,可以接近真实LRU的效果。"
总结与建议
核心:
理解惰性删除 + 定期删除的互补关系,以及内存淘汰策略的多种选择及其应用场景。
高频考点:
allkeys-lru 和 volatile-lru 的区别、LRU 和 LFU 算法的核心思想对比。
实践建议:
生产环境中,通常将Redis作为缓存时使用 allkeys-lru;若同时用于持久化和缓存,则对需要持久化的数据不设TTL,并采用 volatile-lru 等策略。
掌握这些,你就能在面试中清晰、有条理地回答这个问题了。
了解两种高效的定时器方案
1、基于优先级队列/堆

2、基于时间轮实现的定时器

数据结构和内部编码
type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列
表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构
resid的五种数据类型

redis内部编码
redis针对每种数据结构都有自己底层内部编码实现,而且是多种实现,redis会在适合场景选择合适的内部编码
设计好处:
1、可以改进内部编码,而对外部数据结构和命令不产生任何影响,这样一旦开发出更优秀的内部编码,无需改动外部数据结构和命令
2、多种内部编码实现可以在不同场景下发挥各自的优势

注意:可以通过object encoding查询内部编码
单线程架构



为何单线程还能每秒万级的处理能力呢?:
a. 纯内存访问。Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,这是 Redis 达
到每秒万级别访问的重要基础。
b. 非阻塞 IO。Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上 Redis 自身的事件处理模型
将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间
c. 单线程避免了线程切换和竞态产生的消耗。单线程可以简化数据结构和算法的实现,让程序模
型更简单;其次多线程避免了在线程竞争同一份共享数据时带来的切换和等待消耗。
redis的致命缺点:对于单个命令的执行时间都是有要求的。如果某个命令执行过长,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客户端的阻塞
string字符串
常见命令
| 命令 | 语法 | 描述 | 版本 | 时间复杂度 | 返回值 |
|---|---|---|---|---|---|
| set | set key value [ex seconds] [px milliseconds] [nx|xx] |
设置指定 key 的值。可设置过期时间和条件(nx:不存在时设置,xx:存在时设置) | 1.0.0 | o(1) | ok(成功时) |
| get | get key |
获取指定 key 的值。如果 key 不存在,返回 nil。如果 value 不是字符串类型,会报错 | 1.0.0 | o(1) | key 对应的 value,或者 nil(当 key 不存在) |
| mget | mget key1 [key2 ...] |
获取一个或多个给定 key 的值。如果某个 key 不存在,返回 nil | 1.0.0 | o(n) | 一个包含所有给定 key 的值的列表 |
| mset | mset key1 value1 [key2 value2 ...] |
同时设置一个或多个 key-value 对。会覆盖已存在的 key | 1.0.0 | o(n) | 总是返回 ok |
| setnx | setnx key value |
只有在 key 不存在时设置 key 的值。可用于分布式锁实现 | 1.0.0 | o(1) | 1(设置成功),0(设置失败) |
注意关于set
set key value [expiration EX seconds|PX milliseconds] 1 [NX|XX]
SET 命令支持多种选项来影响它的行为:
• EX seconds ------ 使用秒作为单位设置 key 的过期时间。
• PX milliseconds ------ 使用毫秒作为单位设置 key 的过期时间。
• NX ------ 只在 key 不存在时才进行设置,即如果 key 之前已经存在,设置不执行。
• XX ------ 只在 key 存在时才进行设置,即如果 key 之前不存在,设置不执行。
mget和mset
使用 mget / mset 由于可以有效地减少了网络时间,所以性能相较更高。

setnx
| 特性 | SET | SETNX | SETXX |
|---|---|---|---|
| 全称 | Set | Set if Not eXists | Set if eXists |
| 条件 | 无条件设置 | 仅当键不存在时设置 | 仅当键已存在时设置 |
| 原子性 | ✅ 是 | ✅ 是 | ✅ 是 |
| 返回值 | OK | 1-成功,0-失败 | 1-成功,0-失败 |
| 过期时间 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| 版本 | Redis 1.0.0+ | Redis 1.0.0+ | Redis 2.6.12+ |
计数命令
| 命令 | 语法 | 描述 | 版本 | 时间复杂度 | 返回值 |
|---|---|---|---|---|---|
| incr | incr key |
将 key 中储存的数字值加 1。如果 key 不存在,会先初始化为 0 再执行 incr 操作 | 1.0.0 | o(1) | 执行 incr 之后 key 的值 |
| incrby | incrby key increment |
将 key 中储存的数字值加上指定的增量值。如果 key 不存在,会先初始化为 0 再执行 incrby 操作 | 1.0.0 | o(1) | 加上增量之后 key 的值 |
| decr | decr key |
将 key 中储存的数字值减 1。如果 key 不存在,会先初始化为 0 再执行 decr 操作 | 1.0.0 | o(1) | 执行 decr 之后 key 的值 |
| decrby | decrby key decrement |
将 key 中储存的数字值减去指定的减量值。如果 key 不存在,会先初始化为 0 再执行 decrby 操作 | 1.0.0 | o(1) | 减去减量之后 key 的值 |
| incrbyfloat | incrbyfloat key increment |
为 key 中储存的浮点数值加上指定的浮点数增量。如果 key 不存在,会先初始化为 0 再执行 incrbyfloat 操作 | 2.6.0 | o(1) | 加上浮点数增量之后 key 的值 |
其他命令
| 命令 | 语法 | 描述 | 版本 | 时间复杂度 | 返回值 |
|---|---|---|---|---|---|
| append | append key value |
将 value 追加到 key 原来的值的末尾。如果 key 不存在,就相当于 set 操作 | 2.0.0 | o(1) | 追加后字符串值的总长度 |
| getrange | getrange key start end |
返回 key 中字符串值的子字符串,从 start 开始到 end 结束(包含两端)。负数表示从字符串末尾开始计算 | 2.4.0 | o(n) n 是返回字符串的长度 | 截取的子字符串 |
| setrange | setrange key offset value |
用 value 参数覆盖给定 key 所储存的字符串值,从偏移量 offset 开始。如果 key 不存在,就当作空字符串处理 | 2.2.0 | o(1) | 修改后的字符串长度 |
| strlen | strlen key |
返回 key 所储存的字符串值的长度。当 key 储存的不是字符串类型时,返回一个错误 | 2.2.0 | o(1) | 字符串值的长度,如果 key 不存在返回 0 |
内部编码
3 种:
• int:64位/8 个字节的长整型。
• embstr:普通压缩字符串,用于表示比较短的字符串(小于等于 39 个字节的字符串。)
• raw:普通字符串,用于表示更长的字符串,只是单纯的持有字符数组。(大于 39 个字节的字符串。)
Redis 会根据当前值的类型和长度动态决定使用哪种内部编码实现。
典型使用场景
缓存
Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
redis和mysql组成的缓存存储架构

伪代码模拟业务数据访问过程
1、假设业务是根据用户uid获取用户信息
cpp
UserInfo getUserInfo(long uid)
{
.......
}
2、先从redis获取用户信息,假设保存在user:info:<uid>对应的键中
cpp
//根据uid得到redis的键
String key = "user:info:"+uid;
//尝试获取对应的值
String value=Redis执行命令:get key;
//若缓存命中(hit)
if(value!=null)
{
//假设用户信息按照json格式存储
UserInfo userInfo=json反序列化(value);
return userInfo;
}
3、若没有从redis中得到用户信息,及缓存miss,则从MySQL中尝试获取,随后写入缓存并返回
cpp
//缓存未命中
if(vakue==null)
{
UserInfo userInfo=MySQL执行SQL:select * from user_info where uid =<uid>
//如果查找的表中没有uid对应的用户信息
if(userInfo==null)
{
响应404
return null;
}
//命中
//将用户信息序列化为json格式并写入缓存
String value=json序列化(userInfo);
//且防止数据腐烂(rot),设置过期时间3600秒
redis执行命令:set key value ex 3600
//返回用户信息
return userInfo;
}
注意:
与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,而且也没有对键名
有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用 "业务名:对象名:唯一标识:属性" 作为键名。
例如:MySQL 的数据库名 vs,用户表名为 user_info,那么对应的键可以使用:vs:user_info:6379
或者vs:user_info:6379:name来表示
如果键名过长,则可以使用团队内部都认同的缩写替代
计数功能
许多应用都会使用 Redis 作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数
据可以异步处理或者落地到其他数据源。

cpp
// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid)
{
key = "video:" + vid;
long count = Redis 执行命令:incr key
return counter;
}
实际中要开发一个成熟、稳定的真实计数系统,要面临的挑战远不止如此简单:防作弊、按
照不同维度计数、避免单点问题、数据持久化到底层数据源等。
共享会话
一个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成一个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同一台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。

为了解决这个问题,我们使用 Redis 将用户的 Session 信息进行集中管理

手机验证码;

以下为伪代码思路:
cpp
String 发送验证码(phoneNumber) {
key = "shortMsg:limit:" + phoneNumber;
// 设置过期时间为 1 分钟(60 秒)
// 使用 NX,只在不存在 key 时才能设置成功
bool r = Redis 执行命令:set key 1 ex 60 nx
if (r == false) {
// 说明之前设置过该手机的验证码了
long c = Redis 执行命令:incr key
if (c > 5) {
// 说明超过了一分钟 5 次的限制了
// 限制发送
return null;
}
}
// 说明要么之前没有设置过手机的验证码;要么次数没有超过 5 次
String validationCode = 生成随机的 6 位数的验证码();
validationKey = "validation:" + phoneNumber;
// 验证码 5 分钟(300 秒)内有效
Redis 执行命令:set validationKey validationCode ex 300;
// 返回验证码,随后通过手机短信发送给用户
return validationCode ;
}
// 验证用户输入的验证码是否正确
bool 验证验证码(phoneNumber, validationCode) {
validationKey = "validation:" + phoneNumber;
String value = Redis 执行命令:get validationKey;
if (value == null) {
// 说明没有这个手机的验证码记录,验证失败
return false;
}
if (value == validationCode) {
return true;
} else {
return false;
}
}
哈希
详细解释redis hash
在 Redis 中,哈希类型是指值本身又是一个键值对结构,形如 key = "key",value = { {field1, value1 }, ..., {fieldN, valueN } }
可以理解成键值对中的键值对。在 Hash 中,一个键对应一个映射表,表里可以有很多个字段(field),每个字段有自己的值。
bash
redis-key → { field1: value1, field2: value2, ... fieldN: valueN }
| 部分 | 说明 | 示例 |
|---|---|---|
| 外层键 | Hash 在 Redis 中的唯一标识 | user:1001 |
| **字段(field)** | Hash 内部的键名 | username, age, email |
| **值(value)** | 字段对应的值 | "张三", "25", "zhang@example.com" |
例如,用一个 Hash 来存储用户信息(用户ID为 1001):
bash
# 存储成 Hash
HSET user:1001 name "张三" age 30 city "北京"
# 相当于键`user:1001`内部存储了:
# {
# "name": "张三",
# "age": "30",
# "city": "北京"
# }
Redis 键值对和哈希类型二者的关系如图:
哈希
| 维度 | String 普通键值 | Hash |
|---|---|---|
| 存储内容 | 一个字符串(可存 JSON) | 多个字段-值对 |
| 局部更新 | 需整体替换 | 可单独更新字段 |
| 取部分数据 | 需读取整个值 | 可只取一个字段 |
| 内存效率 | 简单场景更直接 | 对象类数据更省内存 |
| 适用场景 | 单个值、计数器、序列化数据 | 对象、配置项、多属性实体 |
命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| hset | 设置hash中指定的字段(field)的值(value) | hset key field value [field value ...] |
插入一组field为O(1) 插入N组field为O(N) | 添加的字段的个数 |
| hget | 获取hash中指定字段(field)的值(value) | hget key field |
O(1) | 字段的值,如果字段不存在或key不存在则返回nil |
| hexists | 检查hash中指定的字段(field)是否存在 | hexists key field |
O(1) | 1(存在) 或 0(不存在) |
| hdel | 删除hash中一个或多个指定字段(field) | hdel key field [field ...] |
删除一个field为O(1) 删除N个field为O(N) | 成功删除的字段数量 |
| hkeys | 获取hash中所有的字段(field)列表 | hkeys key |
O(N),N是hash的大小 | 字段列表,如果key不存在则返回空列表 |
| hvals | 获取hash中所有的值(value)列表 | hvals key |
O(N),N是hash的大小 | 值列表,如果key不存在则返回空列表 |
| hgetall | 获取hash中所有的字段和值 | hgetall key |
O(N),N是hash的大小 | 字段和值交替组成的列表 |
| hmget | 批量获取hash中多个字段的值 | hmget key field [field ...] |
O(N),N是请求的字段数量 | 字段值列表,不存在的字段返回nil |
| hlen | 获取hash中字段(field)的数量 | hlen key |
O(1) | 字段数量,key不存在时返回0 |
| hsetnx | 只在字段(field)不存在时设置其值 | hsetnx key field value |
O(1) | 1(设置成功) 或 0(字段已存在) |
| hincrby | 将hash中指定字段的值增加整数增量 | hincrby key field increment |
O(1) | 增加后的新值 |
| hincrbyfloat | 将hash中指定字段的值增加浮点数增量 | hincrbyfloat key field increment |
O(1) | 增加后的新值(浮点数) |
内部编码
Hash在底层使用两种编码格式,根据数据量自动转换:
1. 两种内部编码格式
| 编码格式 | 适用场景 | 特点 |
|---|---|---|
| **ziplist(压缩列表)** | 字段较少、值较小 | 内存紧凑,连续存储,节省内存 |
| **hashtable(哈希表)** | 数据量大时 | 查找效率高,O(1)时间复杂度 |
|---|
| |
2. 编码转换规则
Redis根据以下配置参数自动选择编码(默认配置):
bash
# 控制ziplist的最大元素数量
hash-max-ziplist-entries 512
# 控制ziplist中每个元素的最大字节数
hash-max-ziplist-value 64
转换条件:
-
当Hash同时满足以下两个条件时,使用ziplist:
-
字段数量 ≤ 512(hash-max-ziplist-entries)
-
每个字段名和字段值的长度 ≤ 64字节(hash-max-ziplist-value)
-
-
如果任意一个条件不满足 ,则自动转换为hashtable
bash
# 这个Hash会使用ziplist编码(字段数≤512,每个值≤64字节)
hset user:1 name "zhang" age "25" city "beijing"
# 查看内部编码
object encoding user:1
# 返回:"ziplist"
bash
# 当字段数量超过512时,转换为hashtable
# 假设hash-max-ziplist-entries设置为3
hset myhash f1 v1 f2 v2 f3 v3
object encoding myhash
# 返回:"ziplist"
hset myhash f4 v4
object encoding myhash
# 返回:"hashtable"
# 当值长度超过64字节时,转换为hashtable
hset myhash2 field "这个字符串长度超过了64字节,所以会触发编码转换..."
object encoding myhash2
# 返回:"hashtable"
| 维度 | ziplist | hashtable |
|---|---|---|
| 内存占用 | 小(无指针,连续存储) | 大(指针开销,内存碎片) |
| 查询性能 | O(n)(需要遍历) | O(1)(直接哈希定位) |
| 插入性能 | 平均O(n),可能需重分配 | 平均O(1) |
| 适用场景 | 小数据量 | 大数据量 |
使用场景
用户信息存储
关系型数据表保存用户信息:

映射关系表示用户信息:

相比于使用 JSON 格式的字符串缓存用户信息,哈希类型变得更加直观,并且在更新操作上变得 更灵活。可以将每个用户的 id 定义为键后缀,多对 field-value 对应用户的各个属性
-
相比JSON字符串存储,可单独更新字段
-
相比多个String键存储,内存更节省
-
字段数量可控,避免大Key问题
伪代码实现
cpp
UserInfo getUserInfo(long uid) {
// 根据 uid 得到 Redis 的键
String key = "user:" + uid;
// 尝试从 Redis 中获取对应的值
userInfoMap = Redis 执行命令:hgetall key;
// 如果缓存命中(hit)
if (value != null) {
// 将映射关系还原为对象形式
UserInfo userInfo = 利用映射关系构建对象(userInfoMap);
return userInfo;
}
// 如果缓存未命中(miss)
// 从数据库中,根据 uid 获取用户信息
UserInfo userInfo = MySQL 执行 SQL:select * from user_info where uid =
<uid>
// 如果表中没有 uid 对应的用户信息
if (userInfo == null) {
响应 404
return null;
}
// 将缓存以哈希类型保存
Redis 执行命令:hmset key name userInfo.name age userInfo.age city
userInfo.city
// 写入缓存,为了防止数据腐烂(rot),设置过期时间为 1 小时(3600 秒)
Redis 执行命令:expire key 3600
// 返回用户信息
return userInfo;
}
哈希类型和关系型数据库的不同之处:
• 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而 关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null

• 关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等 基本不可能,维护成本高。
缓存方式对比
截至目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点 分析。
- 原生字符串类型------使用字符串类型,每个属性一个键。
bash
set user:1:name James set user:1:age 23 set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。 缺点:占用过多的键,内存占用量较大,同时用户信息在 Redis 中比较分散,缺少内聚性,所以这种 方案基本没有实用性。
- 序列化字符串类型,例如 JSON 格式
bash
set user:1 经过序列化后的用户对象字符串
优点:针对总是以整体作为操作的信息比较合适,编程也简单。同时,如果序列化方案选择合适,内 存的使用效率很高。 缺点:本身序列化和反序列需要一定开销,同时如果总是操作个别属性则非常不灵活。
- 哈希类型
bash
hmset user:1 name James age 23 city Beijing
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。 缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较大消耗。
列表
列表类型是用来存储多个有序的字符串。在 Redis 中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、 获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以 充当栈和队强强强强列的角色,在实际开发上有很多应用场景。
列表两端插入和弹出操作

列表的获取、删除等操作

获取第 5 个元素,可以执行 lindex user:1:messages 4 或者倒数第 1 个元素,lindex user:1:messages -1 就可以得到元素 e。
区分获取和删除的区别,例如图 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删 除,这个操作会导致列表的长度从 5 变成 4;但是执行 lindex 4 只会获取元素,但列表长度是不会变化 的。
列表中的元素是允许重复

命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| lpush | 将一个或多个值插入到列表的头部(左边) | lpush key value [value ...] | 插入一个元素为O(1),插入N个元素为O(N) | 执行后列表的长度 |
| lpushx | 将一个值插入到已存在列表的头部,如果列表不存在,操作无效 | lpushx key value | O(1) | 执行后列表的长度(如果列表不存在则返回0) |
| rpush | 将一个或多个值插入到列表的尾部(右边) | rpush key value [value ...] | 插入一个元素为O(1),插入N个元素为O(N) | 执行后列表的长度 |
| rpushx | 将一个值插入到已存在列表的尾部,如果列表不存在,操作无效 | rpushx key value | O(1) | 执行后列表的长度(如果列表不存在则返回0) |
| lrange | 获取列表中指定范围内的元素 | lrange key start stop | O(S+N),S为偏移量start,N为指定范围内的元素个数 | 指定区间内的元素列表 |
| lpop | 移除并返回列表的头部(左边)第一个元素 | lpop key | O(1) | 被移除的元素值,如果列表为空或不存在则返回nil |
| rpop | 移除并返回列表的尾部(右边)第一个元素 | rpop key | O(1) | 被移除的元素值,如果列表为空或不存在则返回nil |
| lindex | 通过索引(下标)获取列表中的元素 | lindex key index | O(N),N为到达索引下标的遍历元素数。对头部或尾部元素操作只需O(1) | 列表中下标为指定索引值的元素,如果索引超出范围则返回nil |
| linsert | 在列表中某个现有元素的前面或后面插入新元素 | linsert key before|after pivot value | O(N),N为找到基准元素pivot所需遍历的元素数。在头部或尾部插入为O(1) | 插入操作完成后列表的长度。如果基准元素pivot不存在,则返回-1,不执行任何操作 |
| llen | 获取列表的长度 | llen key | O(1) | 列表的长度(元素数量),如果key不存在则返回0 |
阻塞版本命令
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应非阻塞版本的作用基本一致,除了:
在列表中有元素的情况下,阻塞和非阻塞表现是一致的。但如果列表中没有元素,非阻塞版本会理 解返回 nil,但阻塞版本会根据 timeout,阻塞一段时间,期间 Redis 可以执行其他命令,但要求执 行该命令的客户端会表现为阻塞状态
阻塞版本的 blpop 和 非阻塞版本 lpop 的区别
一句话区别
-
LPOP:非阻塞 ,队列里没数据就立刻返回nil -
BLPOP:阻塞 ,队列里没数据就挂起等待 ,直到- 有新元素进队列
- 或者超时
命令 作用 语法 时间复杂度 返回值 blpop 阻塞地从多个列表的**头部(左边)** 弹出元素。只要给定列表中的任何一个有元素可用,或直到超时。 blpop key [key ...] timeout O(1) 对每个有元素的列表。最坏情况下为 O(N),其中 N 是提供的键的数量。 如果成功弹出,返回一个包含列表键名 和被弹出元素值的双元素列表。如果超时,则返回 nil。 brpop 阻塞地从多个列表的**尾部(右边)** 弹出元素。只要给定列表中的任何一个有元素可用,或直到超时。 brpop key [key ...] timeout O(1) 对每个有元素的列表。最坏情况下为 O(N),其中 N 是提供的键的数量。 如果成功弹出,返回一个包含列表键名 和被弹出元素值的双元素列表。如果超时,则返回 nil。 brpoplpush 从源列表的尾部弹出一个值,将其推入目标列表的头部,并返回这个值。如果源列表没有元素,它会阻塞连接直到有元素可弹出或超时。 brpoplpush source destination timeout O(1) 被转移的元素值。如果在超时时间内没有元素可弹出,则返回 nil。
内部编码
| 实现结构 | 版本 | 描述 | 优点 | 缺点 | 使用条件/转换规则 |
|---|---|---|---|---|---|
| **ziplist (压缩列表)** | Redis 3.2之前 | 一种特殊编码的、连续内存的、顺序型数据结构,旨在节省内存。它不存储指向上一个/下一个节点的指针,而是通过编码来记录前一个节点的长度。 | 1. 内存利用率高 :存储紧凑,无指针开销,是内存友好的高效紧凑型结构 。 2. 对CPU缓存友好。 | 1. 修改效率低 :插入/删除操作需重新分配内存,引发"连锁更新"。 2. 查找操作需遍历,时间复杂度为O(N)。 | 当List对象同时满足以下两个条件 时,使用ziplist: 1. 列表保存的所有元素长度都小于64字节 (由list-max-ziplist-value配置)。 2. 列表保存的元素数量小于512个 (由list-max-ziplist-entries配置)。 |
| **linkedlist (双向链表)** | Redis 3.2之前 | 一个标准的双向链表。每个节点(listNode)都包含指向前驱和后继节点的指针,以及一个指向实际字符串值的指针。 |
1. 插入/删除效率高 ,特别是在两端。 2. 内存分配灵活,无需大片连续内存。 | 1. 内存开销大 :每个节点除了存值,还需要存储前后指针(在64位系统上占用16字节)。 2. 内存碎片化,缓存不友好。 | 当列表对象不满足ziplist的使用条件时,会转换为linkedlist。 |
| **quicklist (快速列表)** | Redis 3.2及之后 | 是ziplist和linkedlist的混合体 ,是List类型的默认实现。其本质是一个双向链表,但链表的每个节点都是一个ziplist。 | 1. 平衡了空间和时间的效率 :既保留了ziplist的高内存利用率,又通过分片(以ziplist为节点)避免了大规模连锁更新的风险。 2. 是现代版本List的主流和推荐结构。 | 结构相对复杂,但无显著缺点。 | 自Redis 3.2起,List类型默认全部使用quicklist实现 。其行为由两个关键参数控制: 1. list-max-ziplist-size:控制每个quicklist节点(即ziplist)的最大容量(元素个数或字节大小)。 2. list-compress-depth:控制quicklist两端节点的LZF压缩深度,以进一步节省内存。 |
使用场景
**消息队列:**Redis 可以使用 lpush + brpop 命令组合实现经典的阻塞式生产者-消费者模型队列, 生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地从队列中 "争抢" 队首元素。通过多个客户端来保证消费的负载均衡和高可用性。
Redis 阻塞消息队列模型

分频道的消息队列 :Redis 同样使用 lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费 者可以通过 brpop 不同的键值,实现订阅不同频道的理念。
分频道阻塞消息队列模型

微博 Timeline
每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用 列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
略
选择列表类型时,请参考: 同侧存取(lpush + lpop 或者 rpush + rpop)为栈 异侧存取(lpush + rpop 或者 rpush + lpop)为队列
set集合
保存多个字符串类型的元素的,但和列表类型不同的是,集合中:
1)元素之间是无序 的
2)元素不允许重复,
Redis 除了支持 集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实 际开发中解决很多问题。
集合类型:

普通命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| sadd | 向集合中添加一个或多个成员。如果成员已存在,则忽略。 | sadd key member [member ...] | O(N), N 是被添加的元素数量。 | 本次操作成功添加的新元素数量(已存在的元素不计算在内)。 |
| sismember | 判断指定元素是否是集合的成员。 | sismember key member | O(1)。 | 如果元素是集合的成员则返回 1, 否则返回 0。 |
| spop | 从集合中随机移除并返回一个或多个元素。 | spop key [count] | 不指定count时为O(1), 指定count时为O(N),N为返回的元素个数。 | 被移除的随机元素。当key不存在时,返回nil。 |
| srandmember | 从集合中随机返回 一个或多个元素,但不删除元素。 | srandmember key [count] | 不指定count时为O(1), 指定count时为O(N),N为返回的元素个数。 | 返回一个随机元素;如果指定了count,则返回一个元素列表。 |
| srem | 从集合中移除一个或多个指定元素。 | srem key member [member ...] | O(N), N 是被移除的元素数量。 | 成功移除的元素数量(不包含不存在的元素)。 |
| smove | 将元素从源集合移动到目标集合。 | smove source destination member | O(1)。 | 如果元素被成功移动则返回 1。如果源集合中不存在该元素,则返回 0。 |
| smembers | 获取集合中的所有成员。 | smembers key | O(N), N 是集合的基数(元素总数)。 | 集合中的所有元素。 |
| scard | 获取集合中元素的数量(集合的基数)。 | scard key | O(1)。 | 集合中元素的数量,如果键不存在则返回 0。 |
集合间的操作
交集(inter)、并集(union)、差集(diff)
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| sinter | 返回多个集合的交集(共有元素)。 | sinter key [key ...] | O(N * M), N 是最小集合的大小,M 是集合的数量。 | 包含交集的列表。 |
| sinterstore | 计算多个集合的交集,并将结果存储到一个新的目标集合中。 | sinterstore destination key [key ...] | O(N * M), N 是最小集合的大小,M 是集合的数量。 | 结果集合中的元素数量。 |
| sunion | 返回多个集合的并集(所有元素)。 | sunion key [key ...] | O(N), N 是所有给定集合的元素数量总和。 | 包含并集的列表。 |
| sunionstore | 计算多个集合的并集,并将结果存储到一个新的目标集合中。 | sunionstore destination key [key ...] | O(N), N 是所有给定集合的元素数量总和。 | 结果集合中的元素数量。 |
| sdiff | 返回第一个集合与其他集合之间的差集(在第一个集合中但不在其他任何集合中的元素)。 | sdiff key [key ...] | O(N), N 是所有给定集合的元素数量总和。 | 包含差集的列表。 |
| sdiffstore | 计算集合之间的差集,并将结果存储到一个新的目标集合中。 | sdiffstore destination key [key ...] | O(N), N 是所有给定集合的元素数量总和。 | 结果集合中的元素数量。 |
内部编码
| 实现结构 | 描述 | 优点 | 缺点 | 使用条件/转换规则 |
|---|---|---|---|---|
| **intset (整数集合)** | 一种特殊编码的、有序的、紧凑的整数数组结构,专门用于存储整数。它会根据存储的整数大小自动选择16位、32位或64位的整数类型。 | 1. 内存效率极高 :数据连续存储,无额外指针开销。 2. 查询效率高 :有序数组支持二分查找,查找时间复杂度为O(logN)。 3. 编码自适应 :根据存储的最大整数自动调整存储大小(升级),但不支持降级。 | 1. 仅支持整数 :无法存储字符串等其他类型。 2. 插入/删除效率低 :需要移动元素,平均时间复杂度O(N)。 3. 大整数影响升级:一旦加入大整数导致升级,所有元素都会占用更大空间。 | 当Set对象同时满足以下两个条件 时,使用intset: 1. 集合中的所有元素都是整数值 。 2. 集合中元素的数量不超过512个 (由set-max-intset-entries配置,默认512)。 |
| **hashtable (哈希表)** | Redis的字典实现,是标准的哈希表结构。在Set中使用时,字典的键(key)存储集合元素,字典的值(value)被设为NULL。 | 1. 支持任意类型 :可以存储字符串、整数等各种类型的元素。 2. 操作效率高 :插入、删除、查找的平均时间复杂度为O(1)。 3. 内存分配灵活:无需大片连续内存空间。 | 1. 内存开销大 :每个元素都需要存储哈希表节点结构,包含指针、哈希值等元数据。 2. 存在哈希冲突 :需要解决哈希碰撞问题。 3. 可能触发rehash:哈希表扩容/缩容时会有性能抖动。 | 当Set对象不满足intset的使用条件 时,会转换为hashtable。具体触发条件: 1. 向集合中添加了非整数类型 的元素。 2. 集合中的元素数量超过512个 (超过set-max-intset-entries配置的值)。 |
使用场景:
集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户 对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。
例如:


Zset 有序集合
有序集合 是Redis中一种特殊的数据结构,它结合了集合(Set) 的去重特性 和列表(List) 的排序特性 ,但排序依据是**分数(score)**而不是索引。
| 特性 | 说明 |
|---|---|
| 元素唯一性 | 集合中的每个元素(member)是唯一的,不能重复 |
| 分数关联 | 每个元素都关联一个浮点数分数(score),可以相同 |
| 有序性 | 元素按照分数从小到大排序(默认),分数相同时按字典序排序 |
| 快速访问 | 可以通过元素值获取分数,通过分数范围获取元素 |
ZSet的两种内部编码
| 实现结构 | 描述 | 优点 | 缺点 | 使用条件 |
|---|---|---|---|---|
| **ziplist (压缩列表)** | 将元素和分数按(score, member)对的形式紧凑存储,相邻元素分数接近时节省空间 | 1. 内存利用率高 :连续存储,无额外指针开销 2. 对CPU缓存友好 | 1. 查找效率低 :需遍历查找,O(N)复杂度 2. 插入/删除需要重新分配内存 | 当ZSet对象同时满足以下条件 时: 1. 元素数量 ≤ 128个(zset-max-ziplist-entries) 2. 所有元素长度 ≤ 64字节(zset-max-ziplist-value) |
| **skiplist + dict (跳表+字典)** | 默认实现。跳表维护有序性,字典提供O(1)的元素查找 | 1. 查找速度快 :范围查找O(logN),单元素查找O(1) 2. 范围操作高效 :支持分数范围查询 3. 插入删除快:O(logN) | 1. 内存开销大:需要存储额外的指针和哈希表结构 | 当ZSet对象不满足ziplist条件时自动转换 |
有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可 以帮助我们在实际开发中解决很多问题。
有序集合中的元素是不能重复的,但分数允许重复。类比于一次考试之后,每个人一定有一 个唯一的分数,但分数允许相同。
列表、集合、有序集合三者的异同点

普通命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| zadd | 向有序集合添加一个或多个成员,或更新已存在成员的分数 | zadd key [nx|xx] [ch] [incr] score member [score member ...] | o(log(n)) | 添加成功的新成员数量(不包括更新的) |
| zcard | 获取有序集合的成员数 | zcard key | o(1) | 有序集合的基数(成员数量) |
| zcount | 计算有序集合中指定分数区间的成员数量 | zcount key min max | o(log(n)) | 分数在指定区间内的成员数量 |
| zrange | 通过索引区间返回有序集合指定区间内的成员(分数从小到大) | zrange key start stop [withscores] | o(log(n)+m) | 指定区间内的成员列表 |
| zrevrange | 通过索引区间返回有序集合指定区间内的成员(分数从大到小) | zrevrange key start stop [withscores] | o(log(n)+m) | 指定区间内的成员列表 |
| zrangebyscore | 通过分数返回有序集合指定区间内的成员(分数从小到大) | zrangebyscore key min max [withscores] [limit offset count] | o(log(n)+m) | 指定分数区间内的成员列表 |
| zpopmax | 移除并返回有序集合中分数最高的成员 | zpopmax key [count] | o(log(n))*m | 被移除的成员和分数 |
| bzpopmax | 阻塞式移除并返回有序集合中分数最高的成员 | bzpopmax key [key ...] timeout | o(log(n)) | 被移除的成员、分数和键名 |
| zpopmin | 移除并返回有序集合中分数最低的成员 | zpopmin key [count] | o(log(n))*m | 被移除的成员和分数 |
| bzpopmin | 阻塞式移除并返回有序集合中分数最低的成员 | bzpopmin key [key ...] timeout | o(log(n)) | 被移除的成员、分数和键名 |
| zrank | 返回有序集合中指定成员的排名(按分数从小到大,从0开始) | zrank key member | o(log(n)) | 成员的排名,不存在返回nil |
| zrevrank | 返回有序集合中指定成员的排名(按分数从大到小,从0开始) | zrevrank key member | o(log(n)) | 成员的排名,不存在返回nil |
| zscore | 获取有序集合中指定成员的分数 | zscore key member | o(1) | 成员的分数值,不存在则返回nil |
| zrem | 移除有序集合中的一个或多个成员 | zrem key member [member ...] | o(log(n))*m | 成功移除的成员数量 |
| zremrangebyrank | 移除有序集合中给定排名区间内的所有成员 | zremrangebyrank key start stop | o(log(n)+m) | 被移除的成员数量 |
| zremrangebyscore | 移除有序集合中给定分数区间内的所有成员 | zremrangebyscore key min max | o(log(n)+m) | 被移除的成员数量 |
| zincrby | 对有序集合中指定成员的分数加上增量 | zincrby key increment member | o(log(n)) | 成员的新分数值 |
集合间操作
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| zinterstore | 计算一个或多个有序集合的交集,并将结果(默认对分数求和)存储到新的有序集合中。 | zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max] | o(nk)+o(mlog(m))。n 是最小有序集合的元素个数,k 是有序集合个数,m 是结果集的元素个数。 | 存储到 destination 的结果有序集合中的成员数量。 |
| zunionstore | 计算一个或多个有序集合的并集,并将结果(默认对分数求和)存储到新的有序集合中。 | zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max] | o(n)+o(m*log(m))。n 是所有给定有序集合的元素总数,m 是结果集的元素个数。 | 存储到 destination 的结果有序集合中的成员数量。 |


内部编码
| 实现结构 | 描述 | 优点 | 缺点 | 使用条件/转换规则 |
|---|---|---|---|---|
| **ziplist (压缩列表)** | 一种特殊编码的、连续内存的顺序型数据结构。在ZSet中,每个元素以(score, member)对的形式顺序存储,且按score值升序排列。 | 1. 内存利用率极高 :数据连续紧凑存储,无额外指针开销。 2. 对CPU缓存友好。 3. 在小数据量下,顺序遍历效率尚可。 | 1. 查询效率较低 :查找、插入、删除需要遍历,平均O(N)。 2. 修改代价高 :插入/删除可能导致内存重新分配和后续元素的移动(连锁更新)。 3. 只适合存储小规模数据。 | 当ZSet对象同时满足以下两个条件 时,使用ziplist: 1. 有序集合保存的元素数量小于128个 (由zset-max-ziplist-entries配置,默认128)。 2. 有序集合保存的所有元素成员的长度都小于64字节 (由zset-max-ziplist-value配置,默认64)。 |
| **skiplist + dict (跳表+字典)** | 默认且主要的实现方式。它**同时使用跳跃表(skiplist)和字典(dict)** 两种数据结构来分别提供有序性和O(1)查找能力。 | 1. 综合性能优秀 :跳表提供范围操作O(logN)和有序性,字典提供O(1)的单元素查找。 2. 支持高效的范围操作 :如ZRANGE、ZRANGEBYSCORE。 3. 结构稳定,适合大数据量。 |
1. 内存开销大 :需要同时维护跳表和字典,以及额外的指针和哈希表结构。 2. 实现相对复杂。 | 当ZSet对象不满足ziplist的使用条件 时,会自动转换为skiplist+dict结构。具体触发条件: 1. 元素数量超过128个 。 2. 任意元素成员的长度超过64字节。 |
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能 是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热 榜:
1)添加用户赞数 例如用户 james 发布了一篇文章,并获得 3 个赞,可以使用有序集合的 zadd 和 zincrby 功能:
zadd user:ranking:2022-03-15 3 james
之后如果再获得赞,可以使用 zincrby:
zincrby user:ranking:2022-03-15 1 james
2)取消用户赞数 由于各种原因(例如用户注销、用户作弊等)需要将用户删除,此时需要将用户从榜单中删除掉,可 以使用 zrem。例如删除成员 tom:
zrem user:ranking:2022-03-15 tom
3)展示获取赞数最多的 10 个用户
此功能使用 zrevrange 命令实现:
zrevrangebyrank user:ranking:2022-03-15 0 9
4)展示用户信息以及用户分数 次功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用 zscore 和 zrank 来实现。
hgetall user:info:tom
zscore user:ranking:2022-03-15 mike
zrank user:ranking:2022-03-15 mike
渐进式遍历
Redis 使用 scan 命令进行渐进式遍历键,进而解决直接使用 keys 获取键时可能出现的阻塞问 题。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执行多次 scan。
对比 keys和 scan:
| 命令 | 工作方式 | 问题 | 适用场景 |
|---|---|---|---|
keys * |
一次性返回所有键 | 1. 如果键数量很大(百万级),会阻塞 Redis 服务 2. 返回数据量可能很大,占用大量内存 | 测试环境、键数量很少的场景 |
scan |
分批返回,每次只返回一小部分 | 1. 不会阻塞 Redis(每次 O(1)) 2. 适合生产环境大数据量场景 | 生产 |
scan 命令渐进式遍历
命令:> scan 0 count 3
返回:游标 "2",键 ["w", "i", "e"]
scan 0:从游标 0 开始遍历(从头开始)
count 3:尝试返回大约 3 个键
返回游标 "2":表示遍历还没结束,下次要从游标 2 继续
返回键 ["w", "i", "e"]:这次遍历找到的 3 个键
命令:> scan 2 count 3返回:游标 "7",键 ["x", "j", "q"]
scan 2:从上次返回的游标 2 继续
返回游标 "7":还需要继续遍历
返回键 ["x", "j", "q"]:新找到的 3 个键
.........

渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删 除),可能导致遍历时键的重复遍历或者遗漏,这点务必在实际开发中考虑。
Stream 类型深入详解
1.1 Stream 类型概述
Redis Stream 是 Redis 5.0 引入的持久化消息队列数据结构,专门为消息流、事件日志和流处理场景设计。它结合了 List、Sorted Set 和 Pub/Sub 的优点,提供了完整的企业级消息队列功能。
1.2 Stream 内部编码
Stream 使用 listpack 作为底层数据结构,这是一种比 ziplist 更高效的紧凑型结构。
| 特性 | 描述 |
|---|---|
| 底层结构 | listpack(列表包),一种紧凑的连续内存结构 |
| 节点组织 | 消息按宏节点(macro node)分组存储,每个宏节点包含多个 listpack 节点 |
| 内存效率 | 连续存储,指针开销小,内存利用率高 |
| 性能特点 | 支持快速的中部插入删除,适合流式数据 |
| 自动管理 | 支持自动裁剪旧消息,防止内存无限增长 |
Stream 内存结构示例:
Stream "order:events"
├── 宏节点1 (ID范围: 0-0 到 1000-0)
│ ├── listpack节点1: ID=0-0, 数据={user_id:1001, action:"login"}
│ ├── listpack节点2: ID=1-0, 数据={user_id:1002, action:"purchase"}
│ └── ...
├── 宏节点2 (ID范围: 1001-0 到 2000-0)
│ └── ...
└── 消费者组信息
├── 消费者组 "notifiers"
└── 消费者组 "analyzers"
1.3 Stream 命令表格
表1-1:Stream 基本操作命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| xadd | 向Stream添加消息 | xadd key [nomkstream] [maxlen=|minid= threshold [limit count]] *|id field value [field value ...] | o(1) | 消息ID |
| xrange | 按ID范围正向读取 | xrange key start end [count count] | o(log(n)+m),n为消息总数,m为返回数 | 消息列表 |
| xrevrange | 按ID范围反向读取 | xrevrange key end start [count count] | o(log(n)+m) | 消息列表(逆序) |
| xlen | 获取消息数量 | xlen key | o(1) | 消息总数 |
| xdel | 删除指定消息 | xdel key id [id ...] | o(1) 每条消息 | 成功删除数 |
| xtrim | 裁剪Stream | xtrim key maxlen|minid [=|=] threshold [limit count] | o(n),n为被删消息数 | 被删消息数 |
表1-2:消费者组相关命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| xgroup create | 创建消费者组 | xgroup create key groupname id|$ [mkstream] | o(1) | OK |
| xreadgroup | 消费者组读取 | xreadgroup group groupname consumername [count count] [block ms] [noack] streams key [key ...] id [id ...] | o(m),m为返回数 | 消息列表 |
| xack | 确认消息处理 | xack key groupname id [id ...] | o(1) 每条消息 | 确认消息数 |
| xpending | 查看待处理消息 | xpending key groupname [[idle min-idle-time] start end count [consumer]] | 复杂度不一 | 待处理消息信息 |
| xclaim | 转移消息所有权 | xclaim key groupname consumername min-idle-time id [id ...] [idle ms] [time ms] [retrycount count] [force] [justid] | o(1) 每条消息 | 转移的消息 |
表1-3:Stream 管理命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| xinfo | 获取Stream信息 | xinfo [consumers key groupname] [groups key] [stream key] [help] | o(n) | 相关信息 |
| xsetid | 设置最后ID | xsetid key id | o(1) | OK |
1.4 Stream 使用场景
场景1:订单状态变更事件流
# 1. 记录订单状态变更
> xadd order:events * order_id "ORD001" user_id "1001" from "pending" to "paid" timestamp 1640995200
"1640995200000-0"
> xadd order:events * order_id "ORD001" user_id "1001" from "paid" to "shipped" timestamp 1640998800
"1640998800000-0"
# 2. 创建不同处理组的消费者
> xgroup create order:events notification-group 0-0
> xgroup create order:events analytics-group 0-0
# 3. 通知服务消费
> xreadgroup group notification-group notifier-1 count 1 streams order:events >
1) 1) "order:events"
2) 1) 1) "1640995200000-0"
2) 1) "order_id"
2) "ORD001"
3) "user_id"
4) "1001"
5) "from"
6) "pending"
7) "to"
8) "paid"
9) "timestamp"
10) "1640995200"
# 4. 分析服务消费
> xreadgroup group analytics-group analyzer-1 count 1 streams order:events >
1) 1) "order:events"
2) 1) 1) "1640995200000-0"
2) 1) "order_id"
2) "ORD001"
3) "user_id"
4) "1001"
5) "from"
6) "pending"
7) "to"
8) "paid"
9) "timestamp"
10) "1640995200"
场景2:实时日志收集
# 1. 多服务发送日志
# 服务A
> xadd app:logs * service "api-gateway" level "info" message "user login" user_id 1001
"1640995200000-0"
# 服务B
> xadd app:logs * service "payment-service" level "error" message "payment failed" error_code "PAY_001"
"1640995201000-0"
# 2. 日志分析消费者组
> xgroup create app:logs log-processors 0-0
# 3. 多个日志处理器并行处理
# 处理器A
> xreadgroup group log-processors processor-1 count 2 streams app:logs >
# 处理器B
> xreadgroup group log-processors processor-2 count 2 streams app:logs >
# 4. 查看错误日志
> xrange app:logs - + | grep "level.*error"
1.5 Stream 高级特性
1.5.1 消息ID的特殊值
# 特殊ID值
> xread streams mystream 0-0 # 从最早的消息开始
> xread streams mystream $ # 从最新的消息开始(只接收新消息)
> xread streams mystream 1640995200000-0 # 从指定ID开始
# 消费者组特殊ID
> xreadgroup group mygroup consumer1 streams mystream >
# > 表示从未传递给当前消费者的消息开始
# 0-0 或指定ID 表示从指定位置开始
1.5.2 自动裁剪策略
# 限制Stream的最大长度
# 方法1:添加时限制
> xadd mystream maxlen 1000 * field1 value1 # 保持最多1000条消息
# 方法2:使用xtrim命令
> xtrim mystream maxlen 1000 # 精确裁剪到1000条
> xtrim mystream maxlen ~ 1000 # 近似裁剪,性能更好
# 方法3:按最小ID裁剪
> xtrim mystream minid 1640995200000-0 # 删除ID小于指定值的消息
Geospatial 地理空间类型详解
2.1 Geospatial 类型概述
Redis Geospatial 是基于 Sorted Set 实现的,用于存储和查询地理位置信息的数据结构。它支持存储经纬度坐标,并可以计算两点之间的距离、查找指定范围内的地点等。
2.2 Geospatial 内部编码
Geospatial 没有独立的内部编码,它完全复用 Sorted Set 的编码(ziplist 或 skiplist+dict),只是:
-
分数(score)被解释为经度+纬度的GeoHash编码值
-
成员(member)存储地点名称
-
通过 GeoHash 算法将二维坐标转换为一维的52位整数
| 特性 | 描述 |
|---|---|
| 底层结构 | 与Sorted Set相同(ziplist或skiplist+dict) |
| 坐标精度 | 52位GeoHash,误差约0.5米 |
| 距离计算 | 使用Haversine公式计算球面距离 |
| 范围查询 | 基于GeoHash前缀匹配,近似矩形范围 |
2.3 Geospatial 命令表格
表2-1:Geospatial 基本操作命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| geoadd | 添加地理坐标 | geoadd key [nx|xx] [ch] longitude latitude member [longitude latitude member ...] | o(log(n)) 每个元素 | 成功添加的成员数 |
| geopos | 获取地理坐标 | geopos key member [member ...] | o(log(n)) 每个元素 | 坐标列表(经度,纬度) |
| geodist | 计算距离 | geodist key member1 member2 [m|km|ft|mi] | o(log(n)) | 距离(默认米) |
| georadius | 按圆心半径查询 | georadius key longitude latitude radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] [asc|desc] [store key] [storedist key] | o(n+log(m)),n为中心半径内元素数,m为Sorted Set元素总数 | 符合条件的成员列表 |
| georadiusbymember | 按成员半径查询 | georadiusbymember key member radius m|km|ft|mi [withcoord] [withdist] [withhash] [count count] [asc|desc] [store key] [storedist key] | o(n+log(m)) | 符合条件的成员列表 |
| geohash | 获取GeoHash值 | geohash key member [member ...] | o(log(n)) 每个元素 | GeoHash字符串列表 |
2.4 Geospatial 使用场景
场景1:附近的人/地点搜索
# 1. 添加地点坐标
> geoadd locations:beijing 116.4074 39.9042 "tiananmen" # 天安门
(integer) 1
> geoadd locations:beijing 116.4039 39.9152 "gugong" # 故宫
(integer) 1
> geoadd locations:beijing 116.3912 39.9074 "qianmen" # 前门
(integer) 1
> geoadd locations:beijing 116.3974 39.9087 "wangfujing" # 王府井
(integer) 1
# 2. 计算两个地点距离
> geodist locations:beijing tiananmen gugong km
"1.2636" # 约1.26公里
# 3. 查找天安门1公里内的地点
> georadius locations:beijing 116.4074 39.9042 1 km withdist withcoord
1) 1) "tiananmen" # 地点
2) "0.0000" # 距离(公里)
3) 1) "116.40741562843322754" # 经度
2) "39.9041999772975989" # 纬度
2) 1) "wangfujing"
2) "0.8469"
3) 1) "116.39743214845657349"
2) "39.90870530684960063"
3) 1) "qianmen"
2) "0.9260"
3) 1) "116.39121228456497192"
2) "39.90737335650200555"
# 4. 按成员查找附近地点(以天安门为中心)
> georadiusbymember locations:beijing tiananmen 2 km withdist asc
1) 1) "tiananmen"
2) "0.0000"
2) 1) "wangfujing"
2) "0.8469"
3) 1) "qianmen"
2) "0.9260"
4) 1) "gugong"
2) "1.2636"
场景2:外卖/打车范围匹配
# 1. 注册司机位置
> geoadd drivers:online 116.4074 39.9042 "driver:001"
> geoadd drivers:online 116.4100 39.9000 "driver:002"
> geoadd drivers:online 116.4050 39.9020 "driver:003"
# 2. 用户下单,查找3公里内可用司机
> georadius drivers:online 116.4080 39.9030 3 km withdist asc count 5
1) 1) "driver:001"
2) "0.1234"
2) 1) "driver:003"
2) "0.4567"
3) 1) "driver:002"
2) "0.9876"
# 3. 获取司机的GeoHash(可用于前端地图显示)
> geohash drivers:online driver:001
1) "wx4g0b7f7c0"
2.5 Geospatial 实现原理
2.5.1 GeoHash 编码原理
# GeoHash 简化示例
# 将经纬度转换为52位整数
def geohash_encode(lon, lat):
# 实际使用52位编码
# 经度范围: [-180, 180] -> 二进制
# 纬度范围: [-90, 90] -> 二进制
# 交替编码,如: 经度位1, 纬度位1, 经度位2, 纬度位2...
return 52位整数
# Redis中的分数存储
zadd locations <geohash_value> "location_name"
2.5.2 范围查询优化
# Redis内部将圆形查询转换为近似矩形查询
# 1. 计算中心点的GeoHash
# 2. 计算半径对应的GeoHash精度等级
# 3. 获取该精度的所有相邻GeoHash区块
# 4. 在Sorted Set中查询这些区块内的点
# 5. 精确计算距离,过滤超出半径的点
2.6 Geospatial 性能注意事项
| 操作 | 性能特点 | 优化建议 |
|---|---|---|
| 添加坐标 | o(log(n)),同Sorted Set | 批量添加使用geoadd一次添加多个 |
| 范围查询 | o(n+log(m)),n为范围内元素数 | 控制查询半径,避免过大范围 |
| 距离计算 | o(log(n)) | 结果可缓存,避免重复计算 |
| 大规模数据 | 随数据量增加,查询变慢 | 按城市/区域分片存储 |
2.7 完整示例:基于Geospatial的简单地图服务
# 地图服务示例
# 1. 初始化地图数据
> del map:beijing:pois
> geoadd map:beijing:pois 116.4074 39.9042 "tiananmen"
> geoadd map:beijing:pois 116.4039 39.9152 "gugong"
> geoadd map:beijing:pois 116.3912 39.9074 "qianmen"
> geoadd map:beijing:pois 116.3974 39.9087 "wangfujing"
> geoadd map:beijing:pois 116.4347 39.9042 "chaoyangmen"
> geoadd map:beijing:pois 116.3388 39.9013 "yuetannanlu"
# 2. 用户服务:记录用户位置
> geoadd users:current 116.4080 39.9030 "user:1001"
# 3. 查找用户附近的兴趣点
# 用户当前位置
> geopos users:current user:1001
1) 1) "116.40800005197525024"
2) "39.90300000000000036"
# 查找2公里内的兴趣点
> georadiusbymember map:beijing:pois user:1001 2 km withdist asc
1) 1) "tiananmen"
2) "0.1234"
2) 1) "wangfujing"
2) "0.8469"
3) 1) "qianmen"
2) "0.9260"
# 4. 查找最近的3个地铁站(假设有地铁站数据)
> geoadd subway:stations 116.4070 39.9045 "tiananmen-east"
> geoadd subway:stations 116.3960 39.9070 "wangfujing-station"
> georadius subway:stations 116.4080 39.9030 2 km withdist asc count 3
1) 1) "tiananmen-east"
2) "0.0892"
2) 1) "wangfujing-station"
2) "0.9231"
# 5. 计算路径距离(多点距离)
> geodist subway:stations tiananmen-east wangfujing-station km
"1.2345"
总结对比
| 特性 | Stream | Geospatial |
|---|---|---|
| 底层实现 | listpack | Sorted Set(ziplist/skiplist+dict) |
| 主要用途 | 消息队列、事件流 | 地理位置存储与查询 |
| 数据结构 | 有序消息流 | 坐标点集合 |
| 查询能力 | 范围查询、消费者组 | 距离计算、半径查询 |
| 典型场景 | 订单系统、日志收集 | 附近的人、地图服务 |
| 时间复杂度 | 大部分o(1)或o(log(n)) | 大部分o(log(n)) |
| 内存效率 | 中高(listpack紧凑) | 中(依赖Sorted Set编码) |
使用建议:
-
需要完整消息队列 功能(确认、重试、消费者组) → 使用 Stream
-
需要地理位置 相关功能 → 使用 Geospatial
-
两者结合可以构建实时地理位置服务(如:外卖配送、打车系统)
这两个高级数据类型大大扩展了 Redis 的应用场景,使其从简单的缓存数据库转变为功能丰富的实时数据处理平台。
HyperLogLog 基数统计类型详解
3.1 HyperLogLog 类型概述
HyperLogLog 是一种概率数据结构 ,用于估计一个集合中不重复元素的数量(基数)。它在极小内存占用(约12KB)下,能够以标准误差约0.81%的精度统计数十亿级别的基数。
| 核心特性 | 说明 |
|---|---|
| 近似计数 | 返回的是近似值,不是精确值 |
| 固定内存 | 每个HyperLogLog约12KB,与数据量无关 |
| 可合并 | 多个HyperLogLog可合并为一个 |
| 不可回溯 | 无法获取原始数据,只能统计基数 |
3.2 HyperLogLog 内部编码
HyperLogLog 有固定的内存结构,使用密集表示法(dense representation)存储。
| 内部结构 | 描述 |
|---|---|
| 编码 | 固定为"hll",无其他编码转换 |
| 内存占用 | 固定16384个寄存器 × 6位 = 12288字节(12KB) |
| 寄存器数量 | 2^14 = 16384个寄存器 |
| 寄存器大小 | 每个寄存器6位,范围0-63 |
| 错误率 | 1.04/√m ≈ 0.81%(m=16384时) |
工作原理:
输入元素 → Hash函数 → 52位哈希值
↓
前14位 → 选择寄存器索引(0-16383)
↓
后38位 → 计算前导零的数量 → 更新寄存器值
↓
合并所有寄存器 → 基数估计
3.3 HyperLogLog 命令表格
表3-1:HyperLogLog 基本命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| pfadd | 将元素添加到HyperLogLog | pfadd key element [element ...] | o(1) 每个元素 | 如果基数估计变化,返回1,否则0 |
| pfcount | 返回HyperLogLog的基数估计值 | pfcount key [key ...] | o(1),多key时为o(n) | 基数的近似值 |
| pfmerge | 将多个HyperLogLog合并为一个 | pfmerge destkey sourcekey [sourcekey ...] | o(n),n为HyperLogLog数量 | 成功返回OK |
3.4 HyperLogLog 使用场景
场景1:网站独立访客统计
# 1. 记录每日UV
> pfadd uv:20240418 user:001 user:002 user:003
(integer) 1
> pfadd uv:20240418 user:001 # 重复用户
(integer) 0
# 2. 统计当日UV
> pfcount uv:20240418
(integer) 3
# 3. 记录多日UV
> pfadd uv:20240419 user:001 user:004
(integer) 1
> pfadd uv:20240420 user:002 user:005
(integer) 1
# 4. 合并统计一周UV
> pfmerge uv:week uv:20240418 uv:20240419 uv:20240420
OK
> pfcount uv:week
(integer) 5 # 实际独立用户:user:001,002,003,004,005
# 5. 实时UV统计(每小时)
> pfadd uv:hourly:14 user:001 user:002
(integer) 1
> pfcount uv:hourly:14
(integer) 2
场景2:搜索关键词去重统计
# 统计不同用户搜索"redis"的次数
> pfadd search:redis:users user:001 user:002 user:003
(integer) 1
> pfadd search:redis:users user:001 user:004
(integer) 1 # user:004是新用户
> pfcount search:redis:users
(integer) 4
# 对比不同关键词的搜索用户数
> pfadd search:python:users user:002 user:003 user:005
(integer) 1
> pfcount search:python:users
(integer) 3
# 统计搜索过redis或python的用户数
> pfmerge search:combined search:redis:users search:python:users
OK
> pfcount search:combined
(integer) 5 # user:001,002,003,004,005
3.5 HyperLogLog 性能对比
| 场景 | 精确方法 | HyperLogLog | 优势 |
|---|---|---|---|
| 1亿UV统计 | Set: 约1.2GB | 12KB | 内存节省99.99% |
| 实时去重 | 需要存储全量数据 | 固定内存 | 可处理无限数据 |
| 分布式统计 | 需要数据汇总 | 可合并估算 | 网络传输量小 |
| 长期统计 | 数据积累占用空间 | 空间恒定 | 适合长期监控 |
Bitmap 位图类型详解
4.1 Bitmap 类型概述
Bitmap(位图)是 String 类型的扩展,将字符串解释为位数组。每个位只能存储0或1,非常适合表示布尔状态和进行位级操作。
| 核心特性 | 说明 |
|---|---|
| 底层类型 | String类型,支持SDS编码 |
| 位操作 | 支持按位与、或、非、异或 |
| 偏移范围 | 0 到 2^32-1(最大512MB) |
| 内存效率 | 1位存储1个布尔值,极高效率 |
4.2 Bitmap 内部编码
Bitmap 使用 String 的编码,根据数据情况自动选择。
| 编码类型 | 使用条件 | 内存特点 |
|---|---|---|
| int | 数值较小且可表示为整数 | 共享对象,极小内存 |
| embstr | 长度≤44字节的字符串 | 紧凑存储 |
| raw | 长度>44字节的字符串 | 动态分配 |
内存计算:
1亿用户在线状态:
100,000,000位 ÷ 8 = 12,500,000字节 ≈ 12MB
对比Set存储:
100,000,000 × 8字节(指针)≈ 800MB
节省约98.5%内存
4.3 Bitmap 命令表格
表4-1:Bitmap 基本命令
| 命令 | 作用 | 语法 | 时间复杂度 | 返回值 |
|---|---|---|---|---|
| setbit | 设置指定位的值 | setbit key offset value | o(1) | 原来的位值 |
| getbit | 获取指定位的值 | getbit key offset | o(1) | 位值(0或1) |
| bitcount | 统计值为1的位数 | bitcount key [start end] | o(n) | 1的个数 |
| bitpos | 查找第一个指定位 | bitpos key bit [start] [end] | o(n) | 位置索引 |
| bitop | 位运算(AND/OR/XOR/NOT) | bitop operation destkey key [key ...] | o(n) | 结果长度 |
4.4 Bitmap 使用场景
场景1:用户签到系统
# 1. 用户每月签到记录(每月31天)
# 用户1001在4月1日、3日、5日签到
> setbit sign:202404:1001 0 1 # 4月1日
(integer) 0
> setbit sign:202404:1001 2 1 # 4月3日
(integer) 0
> setbit sign:202404:1001 4 1 # 4月5日
(integer) 0
# 2. 查询签到情况
> getbit sign:202404:1001 0
(integer) 1
> getbit sign:202404:1001 1
(integer) 0 # 4月2日未签到
# 3. 统计当月签到天数
> bitcount sign:202404:1001
(integer) 3
# 4. 统计连续签到天数(从今天往前找第一个0)
# 假设今天是4月6日(偏移5),查找从0-5第一个0的位置
> bitpos sign:202404:1001 0 0 5
(integer) 1 # 第2天是0,所以连续签到1天(从4月5日到4月5日)
# 5. 活跃用户统计(多个用户签到情况)
> setbit sign:202404:1002 0 1
> setbit sign:202404:1003 0 1
# 统计4月1日哪些用户签到
> bitop and sign:20240401:active sign:202404:1001 sign:202404:1002 sign:202404:1003
(integer) 1
> bitcount sign:20240401:active
(integer) 1 # 结果是位图,需要解析
场景2:用户在线状态
# 1. 记录用户在线状态(用户ID作为偏移量)
> setbit online:users 1001 1
(integer) 0
> setbit online:users 1002 1
(integer) 0
> setbit online:users 1003 1
(integer) 0
# 2. 用户1001下线
> setbit online:users 1001 0
(integer) 1
# 3. 统计在线用户数
> bitcount online:users
(integer) 2
# 4. 查找前10000个用户中第一个在线的
> bitpos online:users 1 0 9999
(integer) 1002 # 用户1002
# 5. 批量操作:设置多个用户在线
# 通过pipeline批量设置
4.5 Bitmap 高级应用
布隆过滤器实现
# 简单布隆过滤器实现
# 使用多个哈希函数,对应多个Bitmap
# 1. 添加元素
def bloom_add(element):
h1 = hash1(element) % 1000000
h2 = hash2(element) % 1000000
h3 = hash3(element) % 1000000
redis.setbit("bloom:filter1", h1, 1)
redis.setbit("bloom:filter2", h2, 1)
redis.setbit("bloom:filter3", h3, 1)
# 2. 检查元素是否存在
def bloom_exists(element):
h1 = hash1(element) % 1000000
h2 = hash2(element) % 1000000
h3 = hash3(element) % 1000000
b1 = redis.getbit("bloom:filter1", h1)
b2 = redis.getbit("bloom:filter2", h2)
b3 = redis.getbit("bloom:filter3", h3)
return b1 and b2 and b3
Bitfield 位域类型详解
5.1 Bitfield 类型概述
Bitfield 是 Bitmap 的增强版,允许将字符串解释为多个有符号或无符号的整数数组 ,支持原子性的读、写和自增操作。
| 核心特性 | 说明 |
|---|---|
| 类型丰富 | 支持1-64位的有/无符号整数 |
| 原子操作 | 支持原子读-改-写操作 |
| 溢出控制 | wrap(环绕)、sat(饱和)、fail(失败) |
| 任意偏移 | 可从任意位开始操作 |
5.2 Bitfield 内部编码
Bitfield 使用 String 类型存储,与 Bitmap 相同,但提供了更丰富的操作接口。
| 编码类型 | 描述 | 示例 |
|---|---|---|
| **u<n>** | 无符号n位整数 | u8: 0-255, u16: 0-65535 |
| **i<n>** | 有符号n位整数 | i8: -128-127, i16: -32768-32767 |
| 偏移量 | 位偏移,从0开始 | 操作第n位开始的字段 |
5.3 Bitfield 命令表格
表5-1:Bitfield 操作命令
| 命令 | 作用 | 语法 | 复杂度 | 返回值 |
|---|---|---|---|---|
| bitfield | 复杂位域操作 | bitfield key [get type offset] [set type offset value] [incrby type offset increment] [overflow wrap|sat|fail] | o(1)每个子命令 | 结果数组 |
type格式:
-
u8:8位无符号整数 -
i16:16位有符号整数 -
u63:63位无符号整数 -
等等...
offset格式:
-
数字:位偏移,如
0、8、16 -
#n:第n个type大小的字段,如#0、#1
5.4 Bitfield 使用场景
场景1:存储用户多种状态
# 1. 存储用户状态(总32位)
# 位0-3: 用户类型(4位,0-15)
# 位4-7: 权限等级(4位,0-15)
# 位8-15: 年龄(8位,0-255)
# 位16-31: 积分(16位,0-65535)
> bitfield user:1001:status set u4 0 5 set u4 4 2 set u8 8 25 set u16 16 1500
1) (integer) 0
2) (integer) 0
3) (integer) 0
4) (integer) 0
# 2. 读取所有字段
> bitfield user:1001:status get u4 0 get u4 4 get u8 8 get u16 16
1) (integer) 5
2) (integer) 2
3) (integer) 25
4) (integer) 1500
# 3. 原子增加积分(使用溢出控制)
> bitfield user:1001:status overflow sat incrby u16 16 100
1) (integer) 1600
# 4. 如果超过最大值,饱和处理
> bitfield user:1001:status overflow sat incrby u16 16 70000
1) (integer) 65535 # 饱和在最大值
场景2:实时计数器组
# 存储多个计数器,每个8位
# 1. 初始化计数器
> bitfield counters set u8 0 0 set u8 8 0 set u8 16 0
1) (integer) 0
2) (integer) 0
3) (integer) 0
# 2. 原子增加计数器(页面浏览量、点击量、独立访客)
> bitfield counters overflow sat incrby u8 0 1 # PV+1
1) (integer) 1
> bitfield counters overflow sat incrby u8 8 1 # 点击量+1
1) (integer) 1
> bitfield counters overflow sat incrby u8 16 1 # UV+1
1) (integer) 1
# 3. 批量读取所有计数器
> bitfield counters get u8 0 get u8 8 get u8 16
1) (integer) 1
2) (integer) 1
3) (integer) 1
# 4. 使用环绕溢出(循环计数)
> bitfield counters overflow wrap incrby u8 0 300
1) (integer) 45 # 1+300=301, 301%256=45
5.5 Bitfield 高级特性
溢出控制策略对比
# 测试不同溢出策略
> del test:counter
> set test:counter "\x00" # 初始值为0的8位无符号整数
# 1. wrap(环绕,默认)
> bitfield test:counter overflow wrap incrby u8 0 300
1) (integer) 44 # 0+300=300, 300%256=44
# 2. sat(饱和)
> bitfield test:counter overflow sat incrby u8 0 300
1) (integer) 255 # 饱和在最大值255
# 3. fail(失败)
> bitfield test:counter overflow fail incrby u8 0 300
1) (nil) # 溢出,操作失败
有符号整数操作
# 存储有符号温度值(8位,范围-128到127)
> bitfield sensor:temp set i8 0 25
1) (integer) 0
# 温度下降30度
> bitfield sensor:temp incrby i8 0 -30
1) (integer) -5
# 读取温度
> bitfield sensor:temp get i8 0
1) (integer) -5
五种高级类型对比总结
| 类型 | 主要用途 | 内存效率 | 精确性 | 典型场景 | 核心优势 |
|---|---|---|---|---|---|
| Stream | 消息队列/事件流 | 中高 | 精确 | 订单系统、日志收集 | 完整消息队列功能、消费者组 |
| Geospatial | 地理位置 | 中 | 精确 | 附近的人、地图服务 | 地理计算、范围查询 |
| HyperLogLog | 基数统计 | 极高 | 近似(0.81%) | UV统计、去重计数 | 固定12KB、支持海量数据 |
| Bitmap | 布尔状态 | 极高 | 精确 | 签到、在线状态 | 位操作、内存极致节省 |
| Bitfield | 紧凑整数 | 高 | 精确 | 状态标志、计数器 | 原子操作、溢出控制 |
选择指南
7.1 统计类需求
流程图:
需求 → 是否需要精确结果?
├─ 是 → 数据量大小?
│ ├─ 小 → 使用Set
│ └─ 大 → 使用Bitmap(如果可映射为位)
└─ 否 → 使用HyperLogLog
7.2 消息/事件类需求
流程图:
需求 → 是否需要消息确认、消费者组?
├─ 是 → 使用Stream
├─ 否 → 简单队列即可?
│ ├─ 是 → 使用List
│ └─ 延迟队列? → 使用SortedSet
└─ 地理位置相关 → 使用Geospatial
7.3 状态/标志类需求
流程图:
需求 → 存储什么类型数据?
├─ 布尔值(是/否) → 使用Bitmap
├─ 小整数(多个) → 使用Bitfield
├─ 浮点数/字符串 → 使用Hash
└─ 复杂对象 → 使用Hash或String
性能最佳实践
8.1 HyperLogLog
# 1. 批量添加元素
> pfadd uv:daily user:001 user:002 user:003
# 而不是多次pfadd单个用户
# 2. 定期合并,避免频繁pfmerge
# 每小时合并到每天
> pfmerge uv:20240418 uv:hourly:00 uv:hourly:01 ... uv:hourly:23
# 3. 合理设置误差期望
# 默认误差0.81%足够大部分场景
8.2 Bitmap/Bitfield
# 1. 批量操作使用pipeline
pipeline = redis.pipeline()
for i in range(1000):
pipeline.setbit("online:users", i, 1)
pipeline.execute()
# 2. 控制Bitmap大小
# 定期清理过期数据
> del sign:202403:* # 删除上月签到数据
# 3. Bitfield原子操作
# 使用单个bitfield命令执行多个操作
> bitfield user:status set u4 0 5 set u4 4 2 incrby u8 8 1
8.3 Stream/Geospatial
# 1. Stream合理分片
# 按业务分片,避免单个Stream过大
> xadd order:events:20240418 * ... # 按日期分片
# 2. Geospatial范围查询控制半径
# 避免过大半径查询
> georadius locations:beijing 116.4074 39.9042 10 km # 合理半径
> georadius locations:beijing 116.4074 39.9042 1000 km # 避免过大
九、总结
Redis的五种高级数据类型极大地扩展了其应用场景:
-
Stream:从缓存到消息队列的跃迁,适合事件驱动架构
-
Geospatial:位置智能的基石,支撑LBS应用
-
HyperLogLog:海量数据去重的神器,统计不再受内存限制
-
Bitmap:极致空间效率,百万级状态轻松管理
-
Bitfield:精细化的位控制,嵌入式场景的瑞士军刀
黄金法则:
-
知道你的数据特征和访问模式
-
在精确性和效率之间找到平衡
-
合理利用组合数据结构解决复杂问题
-
监控内存使用和性能指标,持续优化
这些高级特性使得Redis从一个简单的键值存储,演变为一个功能丰富的实时数据处理平台,能够应对现代应用的各种复杂需求。
数据库管理
Redis 提供了几个面向 Redis 数据库的操作,分别是 dbsize、select、flushdb、flushall 命令
Redis 只是用数字作为多个数据库的实现。Redis 默认配置中是有 16 个数据库。select 0 操作会切换到第一个数据库,select 15 会切换到最后一个数据库。0 号数据库和 15 号数据库保存的数据是完全不冲突的,即各种有各自的键值对。默认情况下,我 们处于数据库 0。

清除数据库 flushdb / flushall 命令用于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数 据库。

