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