Redis 核心机制与高可用架构实战指南
Redis 核心数据结构与场景映射
Redis 的数据结构选型直接决定了系统的性能上限与开发复杂度。合理匹配业务场景与底层数据结构,是构建高效缓存架构的第一步。
String(字符串)
最基础且使用频率最高的类型,底层采用 SDS(简单动态字符串)实现,支持二进制安全存储。常见应用包括:
- 对象/配置缓存:通常将 JSON 或 Protobuf 序列化后的字符串存入。支持批量读写(MGET/MSET)以降低网络 RTT。
- 分布式锁基础:利用 SET key value NX PX timeout 原子操作实现基础锁机制,避免程序崩溃导致的死锁。
- 计数器与全局 ID:INCR/INCRBY 命令在分库分表场景下可高效生成递增主键或统计阅读数。
Hash(哈希表)
适合存储结构化的轻量级对象。相比 String 存储 JSON,Hash 支持针对单个字段(Field)的独立读写与增量修改(HINCRBY),显著节省内存与 CPU 开销。
注意事项:避免存储超大 Hash 结构。当 Field 数量突破数万级别时,会引发内存碎片与慢查询。若业务强依赖 Hash 归档,建议按业务维度进行分片(如 user:1:profile, user:1:settings)。
List(列表)
底层为双向链表或 QuickList,支持两端插入弹出。天然契合消息队列与时间轴模型:
- 栈/队列模式:LPUSH + RPOP 实现 FIFO 队列;LPUSH + LPOP 实现 LIFO 栈。
- 阻塞队列:BRPOP/BLPOP 可在队列为空时阻塞等待,常用于异步任务消费端。
- 消息流推送:结合 LRANGE 分页截取,可构建社交媒体 Feed 流。推模式适合粉丝量少的场景,拉模式适合大 V 账号。
Set & ZSet(集合与有序集合)
Set 基于哈希表实现,元素唯一且无序。擅长处理交集、并集、差集运算,常用于:标签系统、共同关注计算、抽奖去重、点赞/收藏记录。
ZSet 在 Set 基础上引入 Score(分值),底层采用跳表(SkipList)+ 哈希表实现,支持范围查询与排行榜。核心命令 ZINCRBY、ZREVRANGE 广泛用于游戏排行、热搜榜单、延迟队列(结合 ZADD 与时间戳)。
持久化机制与容灾策略
Redis 提供 RDB 与 AOF 两种持久化方案,生产环境通常结合使用或启用混合模式。
RDB 快照
在指定时间点生成内存数据的二进制快照。触发方式分为手动(SAVE/BGSAVE)与自动(根据 save 配置)。BGSAVE 采用 COW(写时复制)机制,Fork 子进程后,主进程继续响应写请求,仅在被修改的内存页发生页级拷贝,保证服务不阻塞。
AOF(Append Only File)
以日志形式记录每条写命令。通过 appendfsync 策略控制刷盘频率:
always:同步刷盘,数据零丢失,性能极低。everysec:每秒刷盘,性能与安全性平衡(默认推荐)。no:交由 OS 调度,性能最高,宕机可能丢失数秒数据。
AOF 文件会随时间膨胀,需通过 BGREWRITEAOF 触发重写。重写机制同样基于 Fork 子进程,读取当前内存状态生成紧凑的新 AOF 文件。
混合持久化(Redis 4.0+)
配置 aof-use-rdb-preamble yes 启用。重写时,将重写时刻前的内存数据以 RDB 格式写入文件头部,后续增量变更以 AOF 格式追加。重启时先加载 RDB 快速恢复全量数据,再重放 AOF 增量日志。兼顾了 RDB 的快速恢复与 AOF 的数据完整性。
缓存防护体系与数据一致性
引入缓存层后,必须系统化解决三类典型异常:
1. 缓存雪崩(Avalanche)
大量缓存同时过期或 Redis 节点宕机,导致请求全部穿透至数据库。
- 错峰过期:为 TTL 添加随机偏移量(如 base_ttl + random(0~300)),避免集中失效。
- 高可用架构:搭建主从或集群,配合熔断限流(Sentinel/Redisson/网关层),保障单点故障不影响整体链路。
- 后台异步刷新:热点数据设置永不过期,由独立线程或定时任务在后台异步更新。
2. 缓存击穿(Breakdown)
单个热点 Key 过期瞬间,海量并发请求直接冲击数据库。
- 互斥锁重建:缓存未命中时,通过分布式锁(SETNX)控制单线程回源重建,其余请求等待或返回空值/旧值。
- 逻辑过期:Value 中封装过期时间戳,后台线程感知逻辑过期后异步刷新,对外始终返回有效数据。
3. 缓存穿透(Penetration)
请求查询根本不存在的数据,缓存与数据库均无记录,导致无效查询泛滥。
- 空值缓存:对查不到数据的请求,缓存空对象或默认值,设置较短 TTL。
- 布隆过滤器(Bloom Filter):在写入层将数据 Hash 映射到位数组。查询时先过布隆过滤器,若判定不存在则直接拦截,避免回源。
- 参数校验/限流:在网关或应用层拦截非法参数与恶意高频请求。
4. 缓存与数据库一致性
业界主流采用 Cache-Aside(旁路缓存) 模式:读操作命中返回,未命中则查库并写入缓存;写操作先更新数据库,再删除缓存。
并发不一致场景分析:若采用"先删缓存再更新库",读线程可能在删缓存后、写库前读取到旧数据并覆盖缓存,产生脏数据。若采用"先更新库再删缓存",虽然理论上仍存在并发窗口,但实际发生概率极低(写库耗时远大于读缓存),且配合短 TTL 可实现最终一致性。
删除失败兜底方案:
- MQ 重试机制:更新数据库后发送延迟消息,消费者负责删除缓存。失败则重试,达到阈值后告警。
- Binlog 订阅:引入 Canal 或 Debezium 监听 MySQL binlog,异步解析变更事件并删除对应 Redis Key。保证数据变更可追溯且不阻塞主流程。
客户端生态与脚本化执行
Java 生态中主流的 Redis 客户端各有侧重:
- Jedis:老牌同步阻塞客户端,API 贴合原生命令。多线程需配合连接池使用,不支持异步与 Reactor 模型。
- Lettuce:基于 Netty NIO 构建,线程安全,支持同步/异步/响应式编程。单个连接即可处理高并发,Spring Boot 2.x 起默认采用。
- Redisson:高级分布式对象网格。封装了分布式锁、分布式集合、延迟队列等高级特性,适合复杂分布式场景。
- RedisTemplate:Spring Data Redis 提供的抽象层,底层委托给 Jedis 或 Lettuce。简化序列化配置与模板方法调用。
管道技术(Pipeline)
将多条命令打包发送,服务端处理完毕后统一返回响应,大幅降低网络往返延迟。适用于批量写入或统计聚合场景。
// 重构后的管道批处理示例
Jedis jedis = pool.getResource();
try {
Pipeline batch = jedis.pipelined();
for (int idx = 0; idx < 50; idx++) {
String dataKey = "metric:session:" + idx;
batch.incr(dataKey);
batch.expire(dataKey, 3600);
}
List<Object> results = batch.syncAndReturnAll();
// 处理批量返回结果
} finally {
if (jedis != null) jedis.close();
}
Lua 脚本执行
Redis 内置 Lua 解释器,支持原子性执行多步逻辑。适用于库存扣减、分布式锁续期、复杂条件判断等场景。
// 重构后的限流/配额校验 Lua 脚本
String quotaScript = "local current = tonumber(redis.call('GET', KEYS[1])) or 0 " +
"if current < tonumber(ARGV[1]) then " +
" redis.call('INCR', KEYS[1]) " +
" redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Object status = jedis.eval(quotaScript, Collections.singletonList("rate_limit:api_100"),
Arrays.asList("100", "60"));
注意:Lua 脚本在单线程模型下执行,严禁包含死循环或耗时计算,否则会导致整个实例阻塞。
高可用拓扑与集群原理
Redis 高可用方案演进路径:单机 → 主从 → 哨兵 → Cluster 集群。
主从复制
Master 负责读写,Slave 只读。首次全量同步时,Master 执行 BGSAVE 生成 RDB,期间新写入命令缓存至 backlog。RDB 传输完毕后,Slave 加载内存,Master 再将 backlog 增量命令推送,完成同步。后续通过 PSYNC 实现断点续传。
Sentinel 哨兵
独立进程监控 Master/Slave 状态。通过主观下线(SDOWN)与客观下线(ODOWN)判断故障,选举 Leader 后自动执行故障转移(Promote Slave to Master)。客户端需实现 Pub/Sub 监听哨兵通知,动态切换连接地址。
Redis Cluster
去中心化架构,数据按 Slot 分片。默认 16384 个哈希槽,客户端通过 CRC16(key) % 16384 计算槽位并路由。支持自动故障转移、Gossip 协议通信(端口默认 10000 偏移)。
// 重构后的集群客户端初始化示例
Set<HostAndPort> clusterNodes = new HashSet<>();
clusterNodes.add(new HostAndPort("10.0.1.11", 7001));
clusterNodes.add(new HostAndPort("10.0.1.12", 7002));
clusterNodes.add(new HostAndPort("10.0.1.13", 7003));
JedisPoolConfig poolConf = new JedisPoolConfig();
poolConf.setMaxTotal(30);
poolConf.setMinIdle(5);
poolConf.setMaxWaitMillis(2000);
try (JedisCluster client = new JedisCluster(clusterNodes, 3000, 2000, 5, poolConf)) {
client.set("shop:item:2049", "{\\"name\\":\\"Widget\\"}\");
System.out.println(client.get("shop:item:2049"));
} catch (Exception e) {
// 处理集群异常或重定向
}
集群扩容与数据倾斜治理:
- 水平扩容:通过 redis-cli --cluster add-node 添加节点后,使用 reshard 命令重新分配 Slot 范围,数据自动迁移。
- 大 Key 治理:单个 Key 体积过大(如超大 Hash/List)会导致阻塞、迁移失败或内存倾斜。建议拆分为多个小 Key,或使用 Scan 渐进式清理。
- 热 Key 分散:单 Key QPS 过高时,可在客户端增加本地 LRU 缓存,或引入 Key 后缀打散(如 item:1001:0, item:1001:1),降低单节点压力。
脑裂与可用性权衡
网络分区可能导致多个 Master 同时提供写服务。配置 min-replicas-to-write 1 与 min-replicas-max-lag 10 可强制要求至少 N 个从节点同步成功才接受写入,牺牲部分可用性换取数据强一致,避免分区恢复后数据大面积丢失。
Spring Boot 集成配置示例
spring:
redis:
cluster:
nodes: 10.0.1.11:7001,10.0.1.12:7002,10.0.1.13:7003
max-redirects: 5
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 5
shutdown-timeout: 100ms
timeout: 3000