加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
https://1024bat.cn/
https://github.com/webVueBlog/fastapi_plus
https://webvueblog.github.io/JavaPlusDoc/
点击勘误issues,哪吒感谢大家的阅读
1) 架构选型(怎么挑)
Sentinel 主从(HA)
1 主 N 从,故障自动切主,不分片。
读多写少、中小规模、业务多键操作多时优先。
Cluster 分片(HA + 扩容)
≥3 主(每主可配从),按 slot 水平扩展。
高 QPS/大数据量/需要横向扩容;接受跨槽限制。
简单记:要分片就 Cluster;不分片但要高可用就 Sentinel。
2) 部署要点(最小可用)
A. Sentinel(示例)
拓扑:
1 master + 2 replicas + 3 sentinels
关键:一致的
requirepass
/masterauth
、Sentinel 监控mymaster
。
B. Cluster(示例)
拓扑:3 主 3 从 起步(6 个节点),槽位 16384 自动分配。
关键:
cluster-enabled yes
、cluster-require-full-coverage no
(避免单分片故障全停)。
容器/云服务直接选官方模板即可,生产务必跨可用区部署。
3) Spring 客户端配置(Lettuce)
Sentinel
spring:
redis:
password: ${REDIS_PWD}
sentinel:
master: mymaster
nodes: r1:26379,r2:26379,r3:26379
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
timeout: 1500ms
Cluster
spring:
redis:
password: ${REDIS_PWD}
cluster:
nodes: c1:6379,c2:6379,c3:6379,c4:6379,c5:6379,c6:6379
max-redirects: 5
lettuce:
pool:
max-active: 300
max-idle: 80
min-idle: 20
timeout: 1500ms
建议:应用层统一访问 Redis,用连接池;不要让客户端/用户直连 Redis。
4) 多节点必备代码范式
4.1 Pipeline(批量合并,降 RTT)
@Autowired StringRedisTemplate srt;
public List<String> batchGet(List<String> keys) {
List<Object> res = srt.executePipelined((RedisCallback<Object>) conn -> {
for (String k : keys) conn.stringCommands().get(k.getBytes(StandardCharsets.UTF_8));
return null;
});
return res.stream().map(o -> o == null ? null : new String((byte[]) o)).toList();
}
4.2 Lua 原子操作(Cluster/Sentinel 通用)
原子读并删(回包一次性取)
String LUA = "local v=redis.call('GET',KEYS[1]); if v then redis.call('DEL',KEYS[1]); end; return v;";
String getAndDelete(String key){
return srt.execute((RedisCallback<String>) c -> {
byte[] b = (byte[]) c.scriptingCommands().eval(LUA.getBytes(), ReturnType.VALUE, 1, key.getBytes());
return b==null?null:new String(b);
});
}
4.3 分布式锁(务必“只解自己的锁”)
String lockKey="lock:order:"+orderId, token=UUID.randomUUID().toString();
Boolean ok = srt.opsForValue().setIfAbsent(lockKey, token, Duration.ofSeconds(5));
if (Boolean.TRUE.equals(ok)) {
try { /* do work */ }
finally {
String LUA_UNLOCK = "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
srt.execute((RedisCallback<Object>) c -> c.scriptingCommands().eval(LUA_UNLOCK.getBytes(),
ReturnType.INTEGER,1,lockKey.getBytes(),token.getBytes()));
}
}
4.4 Cluster 跨键操作:hash tag 规避跨槽
多键需同槽:把公共部分放
{}
,例如:
SET user:{123}:base ...
HSET user:{123}:ext ...
这样
MGET user:{123}:base user:{123}:ext
不会跨槽。
5) 热点与雪崩(多节点场景最常见问题)
热 Key 复制:写 N 份
hot:k#1..N
(Lua 一次写多份),读随机挑一份;写时全部更新/删除。随机过期:TTL 加 ±10% 抖动,避免同时失效。
负缓存:不存在结果也缓存(短 TTL),防穿透。
两级缓存:L1(Caffeine 30–60s)+ L2(Redis 5–10min),并用 Pub/Sub 做 L1 失效广播。
请求合并(SingleFlight) :同 key 同时只允许一个回源,其他等待缓存填充。
6) 百万用户读(多节点扛量要点)
应用横向扩容 + 连接池 + Pipeline(每批 20–100)。
Cluster 分片,让热度自然摊到多个主分片;或热 key 复制打散。
限流/熔断/降级:Redis 慢/故障时返回旧值或空值,快速恢复。
观测:监控 QPS、延时、内存、命中率、慢日志、连接数、同步延迟。
7) 运维清单(上线必配)
持久化:AOF
everysec
+ RDB 定时快照(避免重启数据空)。高可用:Sentinel/Cluster 跨 AZ;从库仅兜底读(对一致性敏感请全读主)。
淘汰策略:
allkeys-lru
或volatile-lru
,容量打满也不至于雪崩。超时:客户端
timeout 1–2s
;重试指数退避。安全:密码、最小网络暴露、TLS(云上打开)。
扩容/reshard:Cluster 用
redis-cli --cluster reshard
平滑迁移槽位;Sentinel 增副本先replicaof
再接管。
8) 常见坑
把 groupId/Topic 绑主机名(那是 Kafka 的坑点,这里提醒:Redis 不要按主机名分散 key 前缀导致运维困难)。
大 Value/大 Hash:拆分分片,控制单值大小;避免阻塞命令。
Scan 误用:Cluster 上
SCAN
只是单分片;全局扫描需遍历节点。一致性错觉:主从延迟导致读旧值;强一致读请走主或用逻辑版本校验。
结论
小而稳:Sentinel 主从;大而强:Cluster 分片。
客户端:统一连接池 + Pipeline + Lua 原子;
业务:hash tag 跨键、两级缓存、热 key 打散、随机 TTL、负缓存;
运维:AOF、监控、限流、降级、跨 AZ。
架构思路(从外到内)
网关 / 应用层吸收流量
终端 → API 网关(Nginx/Ingress)→ 应用 Pod(水平扩容)→ 连接池访问 Redis
应用层做限流/熔断/降级,避免把所有请求砸到 Redis。
两级缓存(L1 本地 + L2 Redis)
L1:应用进程内 Caffeine(纳秒级),短 TTL(几十秒)
L2:Redis(Cluster/哨兵),较长 TTL(1–10 分钟)
更新策略:Cache Aside(写库成功→删缓存);L1 失效通过 Pub/Sub 或 Stream 做失效广播。
Redis 横向扩展
Redis Cluster(推荐):≥ 3 主 3 从;热点分散到不同分片
或 读写分离:主写从读(从库 eventual consistency,非强一致读慎用)
热点 Key 治理:复制/打散/预热,后面详述
批量/合并
同一请求中批量取(
MGET
/Pipeline),应用侧请求合并(coalescing) ,把同秒内对同 Key 的 N 次查询合并成一次对 Redis 的查询。
降级兜底
Redis/网络异常:直接走 L1、返回旧值、或回源 DB + 限流;必要时返回“近似值/空值 + 快速恢复”。
热点 Key 治理(抗打爆关键)
热点复制:同一个 Key 复制成多份:
user:123#1
…#N
,客户端随机读一份;写时 Lua 更新/删除全部副本。哈希打散:把大 Hash 拆分:
h:{user:123}:0..15
;查时只取命中的分片,避免大对象+单热 key。随机过期:TTL 加随机抖动(±10%),避免雪崩。
负缓存(防穿透) :不存在的结果也缓存短 TTL(如 30s)。
预热:大促/高峰前把热 key 预写进 Redis/L1。
高并发接口设计要点
幂等合并:对同一个
corrId
的重复请求直接返回缓存值;服务内做“在途合并”(SingleFlight)。批量拿:把多用户多 key 合并成一次
MGET
或 Pipeline。结果压缩:值尽量小;必要时启用
CLIENT TRACKING + RESP3
(缓存旁路协同,需新客户端支持)。读写隔离的选择:读延迟敏感可读从,但要接受延迟一致性;强一致就全部读主(性能换一致)。
连接与客户端参数(Lettuce 示例)
连接池:每实例几十~上百个连接足矣;不要为每请求建连接。
Pipeline:把 10~100 个 GET 合并,单次 RTT 取回。
I/O 线程(Redis 6+):开启
io-threads
(只对读有效)。关键参数:
timeout
≥ p99 RTT,maxTotal
(连接池大小)≈实例目标QPS × p95延迟(秒) × 安全系数1.5
tcp-keepalive
、somaxconn
、net.ipv4.ip_local_port_range
合理调大Redis 侧
maxclients
足够大(留给从库/运维)
容量/拓扑粗算(示例)
目标:100 万 DAU、峰值 100k QPS 读
部署:应用 50 实例(2k QPS/实例),Redis Cluster 6 主 6 从
分区:热 key 复制 4 份(读打散),或经业务维度自然分片
连接池:每实例 100 连接,Pipeline 批量 20~50,Redis 单分片 20k–40k QPS 轻松扛
代码示例
1) L1+L2 二级缓存(Spring + Caffeine + Redis)
// build.gradle // implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' // implementation 'org.springframework.boot:spring-boot-starter-data-redis' // L1 本地缓存 Cache<String, String> l1 = Caffeine.newBuilder() .maximumSize(200_000) .expireAfterWrite(Duration.ofSeconds(45)) .build(); @Autowired StringRedisTemplate srt; // 读:先 L1,miss 则 L2,仍 miss 回源(尽量避免 DB) public String getUserProfile(String uid) { String k = "user:prof:" + uid; return l1.get(k, _unused -> { String v = srt.opsForValue().get(k); if (v == null) { v = loadFromOrigin(uid); // 回源(谨慎加限流) if (v != null) srt.opsForValue().set(k, v, Duration.ofMinutes(5)); } return v == null ? "" : v; }); } // 写:写库成功 → 删除 L2 → 通过 Pub/Sub 通知各实例删 L1 public void updateUserProfile(String uid, String json) { writeDB(uid, json); String k = "user:prof:" + uid; srt.delete(k); srt.convertAndSend("cache:invalidate", k); } // 订阅失效广播,删 L1 @EventListener(ApplicationReadyEvent.class) public void sub() { srt.getRequiredConnectionFactory().getConnection() .pubSubCommands().subscribe("cache:invalidate".getBytes()); srt.getConnectionFactory().getConnection().setPubSubListener(new RedisPubSubAdapter() { @Override public void onMessage(byte[] ch, byte[] msg) { l1.invalidate(new String(msg)); } }); }
2) 批量 Pipeline(把 N 次 GET 合成 1 次)
public List<String> batchGet(List<String> keys) { List<Object> res = srt.executePipelined((RedisCallback<Object>) conn -> { for (String k : keys) conn.stringCommands().get(k.getBytes(StandardCharsets.UTF_8)); return null; }); return res.stream().map(o -> o == null ? null : new String((byte[]) o, StandardCharsets.UTF_8)).toList(); }
3) 热点复制 & 原子更新(Lua 更新所有副本)
// 读:随机挑一份 String readHot(String baseKey, int replicas) { int idx = ThreadLocalRandom.current().nextInt(replicas) + 1; return srt.opsForValue().get(baseKey + "#" + idx); } // 写:Lua 同步更新 N 份 + 设 TTL private static final String LUA_SET_ALL = "for i=1,ARGV[2] do redis.call('SETEX', KEYS[1]..'#'..i, tonumber(ARGV[3]), ARGV[1]); end; return 1;"; public void writeHotAll(String baseKey, String val, int replicas, int ttlSec) { srt.execute((RedisCallback<Object>) c -> c.scriptingCommands().eval( LUA_SET_ALL.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, 1, baseKey.getBytes(StandardCharsets.UTF_8), val.getBytes(StandardCharsets.UTF_8), String.valueOf(replicas).getBytes(StandardCharsets.UTF_8), String.valueOf(ttlSec).getBytes(StandardCharsets.UTF_8) )); }
4) 负缓存(防穿透)
String v = srt.opsForValue().get(k); if (v == null) { v = loadFromOriginOrNull(); srt.opsForValue().set(k, v == null ? "__NULL__" : v, Duration.ofSeconds(30)); } if ("__NULL__".equals(v)) return null;
运维与容灾
AOF everysec 打开,避免进程宕机数据丢;
Sentinel/Cluster 跨可用区,故障自动切主;
监控:命中率、内存、水位、QPS、慢查询、p99、连接数、重连次数;
随机过期 与 限流 防雪崩;
预案:Redis 不可用时应用降级(L1 或静态/兜底)。
一句话总结
百万用户读 Redis 的关键在于:用户不直连、两级缓存、热点治理、批量合并、Redis 集群化、连接池 + Pipeline、限流降级。
做到这些,配合合理的容量和监控,百万级并发读是可稳稳拿下的。