加群联系作者vx:xiaoda0423
仓库地址:https://webvueblog.github.io/JavaPlusDoc/
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
go
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
go
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)
go
@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 通用)
原子读并删(回包一次性取)
go
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 分布式锁(务必"只解自己的锁")
go
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)
go// 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 次)
gopublic 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 更新所有副本)
go// 读:随机挑一份 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) 负缓存(防穿透)
goString 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 、限流降级。
-
做到这些,配合合理的容量和监控,百万级并发读是可稳稳拿下的。