深入剖析Redis Cluster集群,Redis持久化机制,Redis数据类型及其数据结构
一、Redis Cluster 高可用部署方案
1. 部署拓扑设计(推荐)
为了保证高可用 + 扩展性 + 性能,建议采用:
6 主 6 从结构(12 实例)
每个主节点管理 2,738 个 slot,总计 16,384 个 slot
节点分布:
┌─────────────┬──────────────┐
│ 主节点 M1 │ 从节点 S1(备份 M1)│
│ 主节点 M2 │ 从节点 S2(备份 M2)│
│ 主节点 M3 │ 从节点 S3(备份 M3)│
│ 主节点 M4 │ 从节点 S4(备份 M4)│
│ 主节点 M5 │ 从节点 S5(备份 M5)│
│ 主节点 M6 │ 从节点 S6(备份 M6)│
└─────────────┴──────────────┘
建议部署方式:
环境 | 部署建议 |
---|---|
云环境(K8s) | 每台机器部署一个 Pod,资源隔离 |
物理机或虚拟机 | 每台部署两个实例(一个主一个从,非互为主从) |
容器环境 | Docker + 网络固定映射(需注意端口) |
2. 端口规划
每个 Redis 实例需要开放:
-
主端口(默认 6379)
-
集群总线端口(主端口 + 10000) → 16379
例如:
6379 / 6380 / 6381 ... → 对应 Redis 实例
16379 / 16380 / 16381 ...→ 用于集群心跳、failover 等通信
3. 目录结构建议
/data/redis/└── 6379/├── redis.conf├── dump.rdb├── appendonly.aof├── logs/└── run/
每个端口一个独立目录。
4. 关键配置项(redis.conf)
最小配置示例(用于集群节点):
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
appendfilename "appendonly.aof"
dbfilename dump.rdb
dir /data/redis/6379
bind 0.0.0.0
protected-mode no
daemonize yes
logfile "/data/redis/6379/logs/redis.log"
✅ 注意:Redis Cluster 模式下必须开启 AOF 或 RDB,否则迁移和重启数据可能丢失。
5. 启动集群节点
假设你启动了以下 6 个主节点和 6 个从节点:
redis-server /data/redis/6379/redis.conf
redis-server /data/redis/6380/redis.conf
...
6. 构建 Redis Cluster
使用 redis-cli --cluster
一键创建集群:
redis-cli --cluster create \192.168.0.1:6379 192.168.0.2:6379 192.168.0.3:6379 \192.168.0.4:6379 192.168.0.5:6379 192.168.0.6:6379 \192.168.0.1:6380 192.168.0.2:6380 192.168.0.3:6380 \192.168.0.4:6380 192.168.0.5:6380 192.168.0.6:6380 \--cluster-replicas 1
自动将 6 个主节点分配 slot、剩余作为从节点。
7. 高可用保障机制
1. 节点宕机自动 failover
-
Redis Cluster 采用内部
Gossip + 选举
协议 -
若主节点宕机,从节点会在
cluster-node-timeout
后自动接管 -
选举由剩余主节点投票完成(多数选举)
2. 客户端自动重定向(MOVED / ASK)
客户端支持 Redis Cluster 协议,自动更新路由映射表。
8. 安全与稳定性建议
项目 | 建议配置 |
---|---|
密码认证 | requirepass + masterauth |
内存限制 | maxmemory + allkeys-lru |
延迟监控 | latency-monitor-threshold 100 |
审计日志 | 配置 logfile 和 rotate |
Redis Sentinel | Redis Cluster 本身已自动选主,不需要 sentinel |
9. 监控指标建议
工具 | 说明 |
---|---|
Prometheus + Redis Exporter | 监控内存、连接数、命中率、slot 分布等 |
Grafana | 可视化面板 |
自研监控 | 重点监控 cluster_state , connected_slaves , instantaneous_ops_per_sec |
10. 调优建议
1. 提前规划 slot 分布
使用 --cluster-slots
指定 slot 范围,避免集中热点。
2. Key 设计防跨 slot
使用 Hash Tag,如:
sign:{123}:20250609
order:{uid123}:create
确保 {}
内的内容一致即可定位到同一 slot,支持多 key 操作(如 Lua 脚本)。
11. 常用命令
命令 | 说明 |
---|---|
redis-cli -c -h host -p port | 连接 cluster 节点 |
cluster nodes | 查看节点状态 |
cluster slots | 查看 slot 分布 |
cluster info | 查看集群状态 |
redis-cli --cluster check | 检查集群一致性 |
redis-cli --cluster fix | 自动修复 slot 问题 |
12. 小结
模块 | 推荐方案 |
---|---|
集群拓扑 | 6 主 6 从 |
数据结构 | Hash Tag 防跨 slot |
部署方式 | 容器化 / 多端口隔离 |
容灾机制 | 自动选主 + AOF |
管理工具 | redis-cli --cluster 、Exporter |
高并发 | 分区热点、避免集中访问 |
二、Redis-Cluster集群中数据的读写流程
在 Redis Cluster 中,写入数据的查找过程是通过一种称为 "分片(sharding)+槽位(hash slot)+节点路由" 的机制完成的。这种机制既保证了分布式扩展能力,又保证了较高的效率。
1. 核心概念
1.1 集群槽位(Hash Slot)
-
Redis Cluster 将所有数据 key 映射到
0~16383
(共 16384 个槽位)。 -
每个节点负责若干个槽位的写入、查询和删除。
-
key 是通过
CRC16(key) mod 16384
算出来的。
1.2 节点
-
Redis Cluster 中的每个节点负责一部分槽位(比如节点 A 负责 0~5000)。
-
集群中包含主节点(Master)和从节点(Slave),主节点负责写入操作,从节点用于备份与故障切换。
2. 写入流程详细剖析
假设我们写入一个 key:set user:123 "Tom"
,以下是详细过程:
步骤 1:客户端计算 key 的槽位
slot = CRC16("user:123") % 16384
比如计算结果是 4567
,Redis 客户端会尝试去访问负责 slot 4567
的节点。
⚠️ Redis 允许使用“哈希标签”来固定 key 到同一个 slot,例如:
set user:{123}:name Tom
和set user:{123}:age 20
会被 hash 到同一个槽位。
步骤 2:客户端从路由表中查找负责这个 slot 的节点
客户端(比如 JedisCluster
、Lettuce
、Redisson
)在初始化连接时,会从任一节点获取整张路由表:
> CLUSTER SLOTS
返回内容示例:
1) 1) (integer) 02) (integer) 54603) 1) "192.168.1.101"2) (integer) 7000
2) 1) (integer) 54612) (integer) 109223) 1) "192.168.1.102"2) (integer) 7001
说明:
-
0 ~ 5460
的 slot 属于192.168.1.101:7000
-
5461 ~ 10922
属于192.168.1.102:7001
-
...其余依此类推
客户端将这个信息缓存起来,后续操作中直接路由到正确节点,减少中转。
步骤 3:客户端直接将命令发送到对应的节点
根据 slot 映射,客户端直接将命令 SET user:123 Tom
发送到对应节点(如 192.168.1.101:7000),该节点执行写入并返回结果。
步骤 4:数据写入节点内存 + AOF/RDB 机制(与单机一致)
在目标节点中,Redis 会:
-
将 key 写入内存(dict)
-
触发 AOF(Append Only File) 或 RDB(快照)机制持久化
-
主节点还会异步将写入同步给从节点
步骤 5:容灾同步(副本机制)
每个主节点都有对应的从节点。写操作默认只写主节点,再由主节点异步复制到从节点(类似 Master-Slave)。
如:
Master A(slot 0~5460) <-- async replicate -- Slave A'
3. 特殊情况:重定向(MOVED、ASK)
3.1 MOVED 重定向
当客户端访问了错误的节点,节点会返回:
-MOVED 4567 192.168.1.102:7001
客户端收到后更新本地路由表,下次访问就直接访问正确节点。
3.2 ASK 重定向(迁移槽位期间)
在 slot 迁移过程中,为了不丢请求,源节点会返回:
-ASK 4567 192.168.1.103:7003
客户端必须先向目标节点发送:
ASKING
SET user:123 Tom
4. 完整流程图(逻辑视图)
客户端 ——> 计算 CRC16(key) % 16384 ——> 查本地槽位路由表│├─ 若命中:直接访问目标 Redis 节点│├─ 若失败:收到 -MOVED,刷新路由重试│└─ 若 slot 迁移中:收到 -ASK,发送 ASKING 命令临时重定向
5. 示例:在 Java 中查看 slot 分配
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.ClusterSlotRange;Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.101", 7000));
JedisCluster cluster = new JedisCluster(nodes);List<Object> slots = cluster.clusterSlots();
for (Object slot : slots) {System.out.println(slot.toString());
}
6. 小结
步骤 | 内容 |
---|---|
1️⃣ | 计算 key 的槽位:CRC16(key) % 16384 |
2️⃣ | 查询本地槽位路由表(CLUSTER SLOTS )找到对应节点 |
3️⃣ | 发送写入命令到目标节点 |
4️⃣ | 数据写入内存 + AOF/RDB |
5️⃣ | 主从同步保证容灾 |
⚠️ | Slot 迁移时用 ASK;访问错误节点会返回 MOVED |
三、深入剖析客户端重定向请求流程(Jedis为例)
在 Redis Cluster 中,当客户端访问了不属于当前连接节点的 slot,会收到 Redis 返回的重定向指令(如 MOVED
),客户端需自动处理重定向并缓存 slot 的正确节点信息,以避免重复跳转,提高性能。
1. 重定向响应类型
Redis Cluster 有两种重定向响应:
类型 | 场景 | 响应格式 | 说明 |
---|---|---|---|
MOVED | slot 被分配到其他节点 | MOVED <slot> <ip:port> | 永久性跳转,需要更新本地 slot 映射表 |
ASK | 临时迁移 slot 过程 | ASK <slot> <ip:port> | 临时跳转,只适用于这一次请求 |
2. 客户端重定向处理流程
以客户端发送以下命令为例:
jedisCluster.set("user:{123}", "OK");
假设客户端连接的是节点 A,但 user:{123}
的 slot 属于节点 B,则:
-
客户端向节点 A 发送请求
-
节点 A 响应:
MOVED 12182 192.168.0.2:6379
-
客户端收到
MOVED
后,更新 slot -> 节点映射缓存 -
下一次再访问 slot
12182
,客户端直接将请求发送到 B,无需再跳转
3. JedisCluster 重定向缓存机制(源码级剖析)
JedisCluster 内部维护一个结构如下的路由表:
Map<Integer, JedisPool> slotCache; // slot → JedisPool(节点连接池)
重定向更新流程:
try {Jedis jedis = slotCache.get(slot).getResource();return jedis.set(key, value);
} catch (JedisMovedDataException movedEx) {// 提取跳转目标节点HostAndPort targetNode = movedEx.getTargetNode();// 更新 slotCache 映射slotCache.put(movedEx.getSlot(), new JedisPool(poolConfig, targetNode.getHost(), targetNode.getPort()));// 重新发起请求return this.set(key, value);
}
✅ JedisMovedDataException 会触发客户端更新 slot → 节点 的映射缓存
4. 举例说明:缓存更新演示
假设:
-
初始 slot
12345
→ 映射到192.168.0.1:6379
-
实际应该为 →
192.168.0.5:6379
访问:
jedisCluster.get("user:{u001}");
Redis 响应:
MOVED 12345 192.168.0.5:6379
Jedis 内部处理:
slotCache.put(12345, JedisPool(192.168.0.5:6379)); // 更新缓存
之后再次访问 slot 12345
,将直接命中正确节点。
5. 与 ASK
的区别
-
MOVED
:客户端更新缓存,永久跳转 -
ASK
:客户端不更新缓存,仅用于迁移期间:
处理方式(Lettuce 示例):
> ASK 12345 192.168.0.3:6379// 客户端执行:
client.send("ASKING"); // 声明一次临时跳转
client.send("GET", "user:{123}");
6. 缓存失效机制
大多数客户端(Jedis、Lettuce)都会:
-
定期刷新 slot 映射(如每 60 秒)
-
在检测到多次
MOVED
后,触发主动更新(防止 slot 分配变化)
7. 小结
步骤 | 客户端行为 |
---|---|
请求到错误节点 | Redis 返回 MOVED |
客户端收到异常 | 解析出 slot 和目标地址 |
更新 slot → 节点缓存 | 存入 slotCache 映射表 |
重发请求 | 访问新的目标节点 |
下一次请求 | 直接命中缓存节点,无需重定向 |
四、深入剖析 Redis 的两种持久化机制:RDB与 AOF
1. RDB(Redis DataBase Snapshot)
✅ 1. 原理概览
RDB 是 Redis 在某一时刻生成整个内存数据快照,持久化为 .rdb
文件。它是基于 fork 的冷快照机制,效率高、数据压缩好。
✅ 2. 触发方式
触发方式 | 描述 |
---|---|
自动 | 配置如 save 900 1 (900 秒至少 1 次写) |
手动 | 命令:SAVE (阻塞),BGSAVE (异步) |
主从同步 | 主机执行 RDB 并传给从机 |
✅ 3. BGSAVE 背后流程(关键)
客户端发起 BGSAVE →
Redis 主进程 fork 子进程 →
子进程将内存快照写入临时文件 →
写入完成后 rename 为 dump.rdb →
主进程继续处理请求
⚠️ fork 会导致主进程短暂阻塞(复制页表),但不影响服务
✅ 4. 优缺点
优点 | 缺点 |
---|---|
高压缩比,恢复速度快 | 恢复时精确到某时间点,不是实时 |
CPU 负载低(周期执行) | fork 时内存消耗大(COW) |
更适合冷备份和主从同步 | 数据可能丢失几分钟 |
2. AOF(Append Only File)
✅ 1. 原理概览
AOF 是将 Redis 所有写命令按顺序记录到日志中,恢复时重放这些命令即可还原数据。
如:SET key1 value1 → 写入 aof 文件
✅ 2. 持久化策略
通过 appendfsync
参数控制写入频率:
模式 | 说明 |
---|---|
always | 每次写操作都 fsync,最安全但最慢 |
everysec(默认) | 每秒 fsync 一次,最佳平衡 |
no | 不主动 fsync,依赖操作系统调度,性能高但风险大 |
✅ 3. AOF 重写机制(rewrite)
随着 AOF 文件不断增长,Redis 会执行 AOF 重写,生成更紧凑版本(去除冗余命令):
原始:
SET x 1
SET x 2
SET x 3重写后:
SET x 3
流程:
-
子进程写入压缩后的 AOF 到新文件
-
主进程仍接收新写入并缓存在 rewrite buffer
-
重写完成后,合并 rewrite buffer 并替换原始 AOF 文件
✅ 4. 优缺点
优点 | 缺点 |
---|---|
数据恢复完整性高(几乎不丢) | 文件增长较快,需定期 rewrite |
可用于操作审计 | 写入性能略低于 RDB |
3. AOF 与 RDB 联合持久化(Redis 推荐)
Redis 允许两者同时启用,策略:
appendonly yes
save 900 1
-
启动时默认优先恢复 AOF(数据更新更及时)
-
RDB 更适合全量冷备
-
可通过配置
aof-use-rdb-preamble yes
:-
AOF 文件前半部分是 RDB 快照,后半是增量命令(极大提升恢复速度)
-
4. 文件格式解析(底层结构)
✅ 1. RDB 文件格式
REDIS
├── Header: "REDIS0009"
├── 数据体(每个 Key 的类型、过期时间、值)
├── EOF
└── CRC64 校验和
RDB 使用自定义二进制格式压缩数据,恢复效率极高。
✅ 2. AOF 文件格式
纯文本命令格式,支持多命令协议:
*3
$3
SET
$4
key1
$5
value
即标准的 Redis 协议格式 RESP,便于重放。
5. 持久化过程分析:写入路径对比
RDB
[客户端] → [Redis Server 内存] → (fork 子进程) → [生成 dump.rdb]
AOF
[客户端] → [内存 + AOF 缓存] → [AOF Buffer] → [fsync 到磁盘]
6. 数据安全性对比(典型故障场景)
场景 | RDB | AOF |
---|---|---|
Redis crash | 丢失上次 BGSAVE 后写入的数据 | 最多丢 1 秒数据(everysec) |
宿主机断电 | 可能没有触发 save | 如果写入缓冲未 fsync,则丢失 |
文件损坏 | 无法恢复 | 可通过 redis-check-aof 修复 |
7. 实际应用建议
场景 | 推荐 |
---|---|
数据恢复速度要求高 | RDB |
数据安全性高 | AOF |
主从同步 | RDB(第一次同步) |
日志审计 | AOF(可追踪所有操作) |
推荐配置 | 同时开启 AOF + RDB |
8. 调优建议
✅ RDB 调优
save 900 1
save 300 10
save 60 10000
避免频繁 fork
(每次触发需考虑内存)
✅ AOF 调优
appendonly yes
appendfsync everysec
aof-rewrite-percentage 100
aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes
9. 总结对比表
特性 | RDB | AOF |
---|---|---|
触发方式 | 定期快照 / 手动 | 实时写操作日志 |
恢复速度 | 快(秒级) | 慢(命令多) |
数据完整性 | 可能丢失 | 几乎不丢 |
文件大小 | 小(压缩好) | 大(命令多) |
性能影响 | 低(周期) | 中(频繁写) |
重启恢复优先级 | 低 | 高 |
五、深入剖析Redis都有哪些数据类型及其数据结构
Redis 的强大性能和灵活性,核心在于其丰富的数据类型和背后的高效数据结构。下面我们从底层实现出发,深入剖析 Redis 各数据类型的编码实现、核心数据结构、操作复杂度、使用场景与优化技巧。
Redis 支持的数据类型总览
数据类型 | 描述 | 内部编码 | 底层数据结构 |
---|---|---|---|
String | 字符串 / 数值 | int / embstr / raw | 简单动态字符串(SDS) |
List | 有序列表 | ziplist / quicklist | 压缩列表 / 快速链表 |
Hash | 字典表 | ziplist / hashtable | 压缩列表 / 哈希表 |
Set | 无序唯一集合 | intset / hashtable | 整数集合 / 哈希表 |
ZSet | 有序集合 | ziplist / skiplist | 压缩列表 / 跳表 + 哈希表 |
Bitmap | 位图 | bit array | 字节数组 |
HyperLogLog | 基数估计 | sparse/dense | 稀疏/密集编码 |
Geo | 地理位置 | sorted set | 跳表结构 |
Stream | 消息队列 | radix tree + listpack | 压缩字典树 |
1. String —— 基本类型,却极其强大
📦 编码方式
编码 | 触发条件 | 描述 |
---|---|---|
int | 值可转为 long 且小于 44 字节 | 使用 long 存储 |
embstr | 小于等于 44 字节 | 分配连续内存块,更高效 |
raw | 大于 44 字节 | 普通 SDS 分配堆内存 |
📚 底层结构:SDS(Simple Dynamic String)
struct sdshdr {int len; // 实际长度int free; // 多预分配空间char buf[]; // 字符数组
}
✅ 支持二进制安全、O(1) 获取长度、自动扩容缩容
⏱ 操作复杂度
-
GET / SET:O(1)
-
APPEND / INCR:O(1) 或 O(N)(扩容时)
2. List —— 双端队列,适合消息队列、任务堆栈等
📦 编码方式
编码 | 条件 | 描述 |
---|---|---|
ziplist | 元素较少,元素较小 | 连续内存,节省空间 |
quicklist(默认) | 统一使用 | 多个 ziplist 的链表,兼顾空间与性能 |
📚 quicklist 结构(Redis 3.2 引入)
quicklist → ziplist → element
-
快速插入/删除:O(1)
-
更低碎片:每个节点存多个元素
⏱ 操作复杂度
-
LPUSH / RPUSH:O(1)
-
LPOP / RPOP:O(1)
-
LINDEX / LRANGE:O(N)
3. Hash —— 轻量级对象存储(如用户信息)
📦 编码方式
编码 | 条件 | 描述 |
---|---|---|
ziplist | key/value 都很短,数量少 | 节省内存 |
hashtable | 元素较多 | 哈希表,高性能查询 |
📚 哈希表结构
dictEntry {void* key;void* value;dictEntry* next; // 链式冲突解决
}
⏱ 操作复杂度
-
HSET / HGET:O(1)
-
HGETALL:O(N)
4. Set —— 无序、唯一集合(如标签系统)
📦 编码方式
编码 | 条件 | 描述 |
---|---|---|
intset | 所有元素为整数 | 整数数组,无 hash 冲突 |
hashtable | 含字符串或数量大 | 常规哈希表 |
📚 intset 优化
-
有序数组 + 二分查找(插入成本稍高,查找快)
-
自动升级类型:int16 → int32 → int64
⏱ 操作复杂度
-
SADD / SREM:O(1)
-
SINTER / SUNION:O(N)
5. ZSet(Sorted Set)—— 有序排行榜核心
📦 编码方式
编码 | 条件 | 描述 |
---|---|---|
ziplist | 元素少,数据短 | 节省空间 |
skiplist | 元素多 | 支持范围查询、排名查询 |
📚 跳表结构
ZSet = 哈希表(member → score)+ 跳表(score 排序)
-
跳表时间复杂度:
-
插入 / 删除:O(log N)
-
区间操作:O(log N + M)
-
-
多层索引节点,快速跳跃查找
6. Bitmap —— 位级存储,节省空间(适合签到、活跃标记)
-
实质:一个大数组的位操作(key 映射到 offset)
-
每 bit 可表示一个状态(如签到 0/1)
-
单条记录消耗 1 bit
⏱ 复杂度
-
SETBIT / GETBIT:O(1)
-
BITCOUNT / BITOP:O(N)
7. HyperLogLog —— 估算去重用户数
-
原理:基于概率算法计算 基数估计(cardinality)
-
精度误差约 ±0.81%
-
每个 key 占用约 12 KB
应用场景
-
日活用户数、IP 去重数估计
-
替代 Set 的场景(当精度要求不高)
8. Geo —— 地理坐标存储与范围查询
-
实现方式:使用 ZSet + Geohash 编码
-
命令:
GEOADD / GEODIST / GEORADIUS
使用场景
-
附近的人/门店推荐
-
范围定位(基于距离)
9. Stream —— 高性能消息队列(Redis 5.0+)
底层结构:Radix Tree(压缩字典树)+ Listpack(紧凑结构)
-
每个 Stream 是一个结构紧凑的时间序列消息队列
-
支持消息 ID、消费组、ack 等机制
应用场景
-
日志收集、消息总线
-
替代 Kafka 的轻量队列方案
10. 编码自动切换机制(重要!)
Redis 为节省内存,会自动选择编码结构,典型如下:
类型 | 小数据结构 | 大数据结构 |
---|---|---|
String | int / embstr | raw |
Hash | ziplist | hashtable |
List | ziplist | quicklist |
Set | intset | hashtable |
ZSet | ziplist | skiplist |
配置项可控制切换阈值,如:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
11. 常见使用场景匹配
场景 | 推荐类型 | 说明 |
---|---|---|
用户属性信息 | Hash | key → field/value |
活跃用户标记 | Bitmap | 节省空间 |
每日签到 | Bitmap / ZSet | 位图 or 排序签到 |
排行榜 | ZSet | 分值决定排名 |
消息队列 | List / Stream | 简单/强需求分别适配 |
用户标签 | Set | 无序唯一集合 |
12. 总结对比表
类型 | 有序? | 可重复? | 底层结构 | 适用场景 |
---|---|---|---|---|
String | ✘ | ✔ | SDS | 缓存、计数器、配置项 |
List | ✔ | ✔ | quicklist | 队列、堆栈 |
Hash | ✘ | key 唯一 | ziplist / dict | 对象字段存储 |
Set | ✘ | ✘ | intset / dict | 标签、唯一集合 |
ZSet | ✔(按 score) | ✘ | skiplist + dict | 排行榜 |
Bitmap | ✔ | ✘ | 字节数组 | 活跃标志、签到 |
HLL | ✘ | ✘ | 计数器 | 去重统计 |
Stream | ✔ | ✔ | radix tree | 消息队列 |
六、如何选择数据类型
1. 从业务语义出发选择数据类型
业务场景 | 推荐数据类型 | 说明 |
---|---|---|
缓存页面内容 / JSON | String | 适用于大段文本、序列化数据 |
计数器 / 限流器 | String | 支持原子自增 INCR/DECR |
用户属性信息(如 name、age) | Hash | 每个字段作为一个小 key |
任务队列 / 消息队列 | List / Stream | 支持先进先出 / 多消费者 |
用户标签、兴趣点 | Set | 无序且唯一 |
排行榜、积分榜 | Sorted Set | 分数决定排名 |
每日签到 / 活跃用户标记 | Bitmap | 每 bit 表示一个用户 |
活跃 IP 数 / 去重统计 | HyperLogLog | 近似去重,占用小 |
附近的人 / 门店搜索 | Geo | 基于 ZSet 做地理计算 |
2. 从访问模式出发选择数据类型
🚀 1. 高频写入(如实时消息、统计)
-
建议使用:
List
/Stream
(推送)或String
(INCR) -
理由:原子操作 + 快速写入 + 空间压缩
🧠 2. 随机访问(如查用户属性)
-
建议使用:
Hash
(如HGET user:123 name
) -
理由:键值小、访问字段不固定、不必分多个 key
📊 3. 排名查询 / 区间检索
-
建议使用:
ZSet
(ZRANGE
、ZREVRANK
) -
理由:天然支持 score 排序,跳表效率高
3. 从数据结构体积出发选择
🚨 Redis 有以下编码切换机制:
-
Hash
使用 ziplist 时节省内存(小数据量) -
超过阈值后自动转成 hashtable(高性能)
建议如下:
类型 | 小数据 | 建议 | 说明 |
---|---|---|---|
Hash | < 512 个 field | 使用 HSET | 紧凑、节省空间 |
List | < 512 元素 | 使用 LPUSH / RPUSH | 快速队列 |
Set | 元素为整数 | 使用 Set 自动编码为 intset |
客户端不需要手动管理编码切换,由 Redis 自动完成。
4. 从功能需求出发选择数据结构
功能 | 推荐类型 | 示例 |
---|---|---|
查询是否存在 | Set / Bitmap | SISMEMBER / GETBIT |
统计总数 | Set / HyperLogLog | 精确 vs 近似去重 |
区间统计 | Sorted Set | ZRANGEBYSCORE |
多用户数据隔离 | 前缀 + 数据类型 | user:1001:tags (Set), user:1001:info (Hash) |
5. 客户端编码示例(Java)
以 Jedis 为例:
🎯 存储用户信息:Hash
Map<String, String> userInfo = new HashMap<>();
userInfo.put("name", "Alice");
userInfo.put("age", "30");
jedis.hmset("user:1001", userInfo);
🎯 排行榜:ZSet
jedis.zadd("scoreboard", 1000, "user1");
jedis.zadd("scoreboard", 2000, "user2");
Set<String> topUsers = jedis.zrevrange("scoreboard", 0, 9);
🎯 签到:Bitmap
int day = 5;
jedis.setbit("sign:user:1001:202506", day, true);
boolean signed = jedis.getbit("sign:user:1001:202506", day);
🎯 活跃用户去重统计:HyperLogLog
jedis.pfadd("uv:2025-06-09", "user1");
long count = jedis.pfcount("uv:2025-06-09");
6. 最佳实践建议
设计原则 | 建议 |
---|---|
业务模型和 Redis 数据结构强关联 | 不要用 String 承载复杂对象 |
利用 key 结构分层管理(如 user:1001:xxx) | 避免 key 冲突 |
大数据分片或拆 key(如 per user / per day) | 避免过大 value 或 key 集合 |
使用 TTL 控制生命周期 | 清理过期数据,防止内存泄漏 |
合理估算结构大小选择类型 | 超过几百万用户用 Bitmap / HyperLogLog 更合适 |
7. 总结图:如何选择 Redis 数据类型?
+-----------------------------+| 有顺序 &重复元素? |+-----------------------------+|Yes | No↓ ↓+----------------+ +----------------+| List | | Set |+----------------+ +----------------+↓ ↓单队列(LPUSH/RPOP) 排名要求?↓ ↓→ List Yes → ZSet(score)No → Set / Bitmap
七、基于Jedis客户端,如何操作各种数据类型
连接初始化(统一前缀)
Jedis jedis = new Jedis("localhost", 6379); // or new JedisPool(...).getResource();
jedis.auth("your_password"); // 如果设置了密码
jedis.select(0); // 选择数据库
1. String:键值存储 / 计数器
// 设置和获取字符串
jedis.set("user:1001:name", "Alice");
String name = jedis.get("user:1001:name");// 自增计数器
jedis.incr("page:view:home"); // 每访问一次加1
2. Hash:存储对象(如用户信息)
// 存储用户信息
Map<String, String> user = new HashMap<>();
user.put("name", "Alice");
user.put("age", "30");
jedis.hmset("user:1001", user);// 获取单个字段或多个字段
String name = jedis.hget("user:1001", "name");
List<String> values = jedis.hmget("user:1001", "name", "age");// 遍历整个 Hash
Map<String, String> all = jedis.hgetAll("user:1001");
3. List:队列/堆栈(如任务、消息队列)
// 从左推入任务
jedis.lpush("task:queue", "task1", "task2");// 从右弹出执行任务
String task = jedis.rpop("task:queue");// 获取列表区间(分页)
List<String> tasks = jedis.lrange("task:queue", 0, 10);
4. Set:无序唯一集合(如标签、兴趣)
// 添加兴趣标签
jedis.sadd("user:1001:tags", "music", "travel");// 是否有某标签
boolean has = jedis.sismember("user:1001:tags", "music");// 获取所有标签
Set<String> tags = jedis.smembers("user:1001:tags");
5. ZSet(Sorted Set):排行榜 / 排名
// 添加分数
jedis.zadd("scoreboard", 1000, "user1");
jedis.zadd("scoreboard", 1200, "user2");// 获取前 N 名用户
Set<String> topUsers = jedis.zrevrange("scoreboard", 0, 9);// 获取某用户排名和分数
Long rank = jedis.zrevrank("scoreboard", "user1");
Double score = jedis.zscore("scoreboard", "user1");
6. Bitmap:签到、活跃标记
// 第5天签到(bit 位偏移)
jedis.setbit("sign:user:1001:202506", 5, true);// 判断是否签到
boolean signed = jedis.getbit("sign:user:1001:202506", 5);// 统计本月总签到天数
long signedDays = jedis.bitcount("sign:user:1001:202506");
7. HyperLogLog:去重统计(如日活)
// 添加用户ID
jedis.pfadd("uv:2025-06-09", "user1", "user2");// 获取近似去重值
long count = jedis.pfcount("uv:2025-06-09");
8. Geo:地理坐标/附近的人
// 添加地理位置
jedis.geoadd("city:store", 116.397128, 39.916527, "Beijing");
jedis.geoadd("city:store", 121.473701, 31.230416, "Shanghai");// 查询距离
Double dist = jedis.geodist("city:store", "Beijing", "Shanghai", GeoUnit.KM);// 附近5公里内地点
List<GeoRadiusResponse> results = jedis.georadius("city:store", 116.397128, 39.916527, 5, GeoUnit.KM);
9. Stream:日志/消息队列(Redis 5.0+)
// 添加一条消息
Map<String, String> message = new HashMap<>();
message.put("event", "login");
message.put("userId", "1001");
jedis.xadd("log:events", StreamEntryID.NEW_ENTRY, message);// 读取最新消息
List<Map.Entry<String, List<StreamEntry>>> streams = jedis.xread(1, 1000, new AbstractMap.SimpleEntry<>("log:events", StreamEntryID.UNRECEIVED_ENTRY));
for (Map.Entry<String, List<StreamEntry>> stream : streams) {for (StreamEntry entry : stream.getValue()) {System.out.println(entry.getID() + " " + entry.getFields());}
}
10. 附加建议
场景 | 类型推荐 | 注意事项 |
---|---|---|
复杂对象 | Hash | field 层级比存 JSON 更高效 |
排行榜 | ZSet | score 控制排序 |
热点标记 | Bitmap | 空间效率极高 |
多端消费 | Stream | 支持消费组 / ack |
八、分析上亿用户连续签到数据的场景,如何使用Redis实现
业务需求与技术挑战
目标 | 技术挑战 |
---|---|
支持上亿用户 | 内存占用低,结构压缩高效 |
支持每日签到 | 支持按天记录签到状态 |
支持连续签到计算 | 需要快速判断连续天数 |
支持查询某天是否签到 | 要求 O(1) 查询 |
高并发 | 写入/查询高吞吐,热点控制 |
可扩展 | 支持多节点,方便水平扩展 |
核心推荐方案:Redis BitMap + Hash 分区 + Lua 脚本
1. 数据结构选择:BitMap
存储每日签到
🧾 设计思路:
-
每个用户一个 BitMap,每一位表示某天是否签到
-
从第 0 位开始,
bitpos = 日期 - 起始日期
(如 2025-01-01) -
例如:
user:sign:12345
的 bitmap010111001...↑第 n 天是否签到
2. Key 设计(分区压缩)
上亿用户数据建议分桶:
sign:{userId % 1024}:{userId}
这样可以:
-
避免 Redis 集群时跨 slot 操作
-
支持多 key 并发写入扩展性强
3. 每日签到命令:SETBIT
SETBIT sign:{userId % 1024}:{userId} offset 1
-
offset = days_since_start(userId, today)
-
设置某天为签到状态
代码示例(Java):
int offset = (int) ChronoUnit.DAYS.between(startDate, LocalDate.now());
String key = String.format("sign:{%d}:%d", userId % 1024, userId);
redisTemplate.opsForValue().setBit(key, offset, true);
4. 查询某天是否签到:GETBIT
GETBIT sign:{userId % 1024}:{userId} offset
5. 统计连续签到天数:Lua 脚本 + BitField
可用 BITFIELD
或 Lua 脚本高效读取连续 N 天位图,例如连续 7 天:
BITFIELD sign:{userId % 1024}:{userId} GET u7 0
用位运算统计从最后一天往前连续 1 的个数:
-- 简化示例:从右往左找连续 1
local key = KEYS[1]
local len = tonumber(ARGV[1])
local count = 0for i = len - 1, 0, -1 doif redis.call('GETBIT', key, i) == 1 thencount = count + 1elsebreakend
end
return count
6. 数据优化:压缩存储
-
每个用户 365 天只需要 365bit ≈ 46B
-
1 亿用户:
46B * 10^8 = 4.6 GB
,非常小
7. 过期清理(可选)
-
使用
ZSet
记录活跃用户(打卡用户) -
每天批量遍历
ZSet
,设置BITMAP
的 TTL 过期策略
8. 常见扩展功能
功能 | 实现方式 |
---|---|
签到排行榜 | 用 ZSet 记录连续签到天数 |
连续签到奖励 | 连续值计算后发奖 |
多端防重 | 使用 SETBIT 幂等性保障 |
补签功能 | 允许用户花币/广告后补位 |
9. 集群与高并发方案
方案 1:分布式集群分片存储(推荐)
-
使用 Redis Cluster,将
sign:{bucket}:{userId}
映射到不同 slot -
保证高并发访问的负载均衡
方案 2:异步化
-
用户签到先写入 Kafka / MQ
-
异步批量落入 Redis,降低高峰写压
10. 小结
目标 | 实现方案 |
---|---|
存储节省 | 使用 BitMap,每用户 46B 一年签到数据 |
查询高效 | GETBIT/SETBIT O(1) 操作 |
连续天数计算 | Lua 脚本或 BITFIELD 快速统计 |
高并发 | 分桶 + Redis Cluster 分布式架构 |
扩展性 | 支持排行榜、补签、领奖逻辑 |