深入剖析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 Tomset user:{123}:age 20 会被 hash 到同一个槽位。


步骤 2:客户端从路由表中查找负责这个 slot 的节点

客户端(比如 JedisClusterLettuceRedisson)在初始化连接时,会从任一节点获取整张路由表:

> 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

  • ...其余依此类推

客户端将这个信息缓存起来,后续操作中直接路由到正确节点,减少中转。


步骤 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 nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.101", 7000));
JedisCluster cluster = new JedisCluster(nodes);

List 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 映射表
ASK 临时迁移 slot 过程 ASK 临时跳转,只适用于这一次请求

2. 客户端重定向处理流程

以客户端发送以下命令为例:

jedisCluster.set("user:{123}", "OK");

假设客户端连接的是节点 A,但 user:{123} 的 slot 属于节点 B,则:

  1. 客户端向节点 A 发送请求

  2. 节点 A 响应:

    MOVED 12182 192.168.0.2:6379
    
  3. 客户端收到 MOVED 后,更新 slot -> 节点映射缓存

  4. 下一次再访问 slot 12182,客户端直接将请求发送到 B,无需再跳转


3. JedisCluster 重定向缓存机制(源码级剖析)

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 → 节点 的映射缓存


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

流程:

  1. 子进程写入压缩后的 AOF 到新文件

  2. 主进程仍接收新写入并缓存在 rewrite buffer

  3. 重写完成后,合并 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. 排名查询 / 区间检索

  • 建议使用:ZSetZRANGEZREVRANK

  • 理由:天然支持 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 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 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 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");

3. List:队列/堆栈(如任务、消息队列)

// 从左推入任务
jedis.lpush("task:queue", "task1", "task2");

// 从右弹出执行任务
String task = jedis.rpop("task:queue");

// 获取列表区间(分页)
List 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 tags = jedis.smembers("user:1001:tags");

5. ZSet(Sorted Set):排行榜 / 排名

// 添加分数
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");

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 results = jedis.georadius("city:store", 116.397128, 39.916527, 5, GeoUnit.KM);

9. Stream:日志/消息队列(Redis 5.0+)

// 添加一条消息
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());
    }
}

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 的 bitmap

    010111001...
          ↑
         第 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 = 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

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 分布式架构
扩展性 支持排行榜、补签、领奖逻辑

你可能感兴趣的:(Redis,redis,架构,数据库)