Redis — 从数据结构到集群原理一、为什么是 RedisRedisREmoteDIctionaryServer是一个基于内存的单线程高性能 KV 数据库。核心指标 读写速度10W QPS / 单节点 操作原子单线程 事件驱动 → 天然线程安全 持久化 RDB快照 AOF日志双保险对比RedisMySQLMongoDB存储内存磁盘磁盘QPS10W几千几千数据结构KV List/Set/Hash/ZSet…表文档持久化RDB AOF原生原生典型用途缓存/队列/计数/分布式锁核心业务数据日志/JSON 存储二、基础数据类型与使用场景Redis 比你想象的更强大 — 它不是简单的 KV而是数据结构服务器类型底层结构典型应用StringSDS动态字符串缓存、计数器、分布式锁HashDict / ListPack用户信息、购物车ListQuickList消息队列、最新列表SetHT / ListPack标签、共同好友ZSetSkipList HT排行榜、延迟队列StreamRadixTree可靠消息队列类似 KafkaBitmapString 位操作签到、布隆过滤器HyperLogLog概率算法UV 统计12KB 估亿级GeoZSet 封装LBS、附近的人2.1 String — 万物皆可存命令GET / SET / INCR / SETNX / SETEX 底层SDSSimple Dynamic StringO(1) 取长度不会溢出// 缓存stringRedisTemplate.opsForValue().set(user:1001,json,30,TimeUnit.MINUTES);// 计数器LongcountstringRedisTemplate.opsForValue().increment(pv:article:123);// 分布式锁BooleanlockedstringRedisTemplate.opsForValue().setIfAbsent(lock:order:orderId,1,10,TimeUnit.SECONDS);2.2 Hash — 对象存储命令HSET / HGET / HGETALL / HINCRBY 底层Dict默认 ListPack小数据量自动切换省内存// 存储用户信息MapString,StringuserMap.of(name,张三,age,28,city,北京);stringRedisTemplate.opsForHash().putAll(user:1001,user);// 只更新某个字段stringRedisTemplate.opsForHash().increment(user:1001,age,1);对比 String 存 JSONHash 可以原子更新单个字段不用读 → 改 → 写。2.3 List — 有序队列命令LPUSH / RPUSH / LPOP / RPOP / LRANGE / BLPOP 底层QuickListLinked List ListPack 混合省内存// 消息队列 — 生产者stringRedisTemplate.opsForList().leftPush(order:queue,orderJson);// 消费者 — 阻塞等待60 秒超时StringmsgstringRedisTemplate.opsForList().rightPop(order:queue,60,TimeUnit.SECONDS);// 最新 10 条动态ListStringlateststringRedisTemplate.opsForList().range(timeline:user:1001,0,9);2.4 Set — 无序去重集合命令SADD / SREM / SINTER / SUNION / SDIFF 底层Dict大 Set ListPack小 Set// 共同好友SetStringcommonFriendsstringRedisTemplate.opsForSet().intersect(friends:user:1,friends:user:2);// 你可能认识的人差集SetStringsuggestionsstringRedisTemplate.opsForSet().difference(friends:user:2,friends:user:1);2.5 ZSet (Sorted Set) — 排行榜神器命令ZADD / ZRANGE / ZREVRANGE / ZRANK / ZINCRBY 底层SkipList跳表 Dict哈希映射查询 O(logN)跳表原理传统链表查找 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 O(N) 跳表 第3层 1 ──────────→ 5 ──────────→ 8 第2层 1 ───→ 3 ───→ 5 ───→ 7 ───→ 8 第1层 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 O(logN)随机层高高层跳跃底层精确定位。// 排行榜 — 新增分数stringRedisTemplate.opsForZSet().add(leaderboard,user:1001,9850);// 排行榜 — 前 10 名倒序SetZSetOperations.TypedTupleStringtop10stringRedisTemplate.opsForZSet().reverseRangeWithScores(leaderboard,0,9);// 延迟队列 — 取到期的任务SetStringtasksstringRedisTemplate.opsForZSet().rangeByScore(delay:queue,0,System.currentTimeMillis());2.6 Stream — 可靠消息队列消息队列演进 List BLPOP → 无 ACK消费者崩溃消息丢失 Pub/Sub → 无持久化断连就丢消息 Stream → 有 ACK 持久化 消费者组≈ 轻量级 Kafka// 发送消息MapString,StringmsgMap.of(orderId,12345,amount,99.9);stringRedisTemplate.opsForStream().add(order:stream,msg);// 消费者组消费ListMapRecordString,Object,ObjectrecordsstringRedisTemplate.opsForStream().read(Consumer.from(order-group,consumer-1),StreamReadOptions.empty().count(2).block(Duration.ofSeconds(5)),StreamOffset.create(order:stream,ReadOffset.lastConsumed()));// 确认消费stringRedisTemplate.opsForStream().acknowledge(order:stream,order-group,recordId);2.7 其余数据类型速览类型关键命令Java 操作BitmapSETBIT / GETBIT / BITCOUNTopsForValue().setBit()HyperLogLogPFADD / PFCOUNTopsForHyperLogLog().add()GeoGEOADD / GEORADIUSopsForGeo().add()三、为什么要用三个节点搭建集群3.1 不是三个节点是三个主节点 至少一个副本Redis Cluster 最少需要六个节点3 主 3 从。三个主节点不是生产建议而是Cluster 协议的最低数学下限。3.2 为什么是 3原因一哈希槽的边界条件Redis Cluster 固定 16384 个哈希槽Slot 每个 Key 通过 CRC16(key) % 16384 落到某个 Slot Master-1 Master-2 Master-3 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 0~5460 │ │ 5461~10922│ │10923~16383│ │ slot │ │ slot │ │ slot │ └──────────┘ └──────────┘ └──────────┘如果只有 2 个主节点一个挂了 一半数据不可用。原因二Raft 共识协议要求奇数3 节点集群至少需要 floor(3/2) 1 2 票才能达成共识 2 节点集群至少需要 floor(2/2) 1 2 票 → 一票也不能少 → 等于没有容错这就是为什么分布式共识协议Raft / Paxos / ZooKeeper全都用奇数节点。原因三脑裂防护网络分区前 Master-1 ←→ Master-2 ←→ Master-3 (3 节点集群) 网络分区后节点 3 被隔离 Master-1 ←→ Master-2 │ Master-3 (孤立) 2/3 → 达成共识继续服务 │ 1/3 → 无法形成多数自动降级 → 集群分区后只有一个分区能提供读写服务避免脑裂四、集群故障选举机制4.1 故障检测流程主观下线PFAIL Node-A 超过 cluster-node-timeout 收不到 Node-B 的 PONG → Node-A 标记 Node-B 为 PFAIL主观怀疑仅自己知道 客观下线FAIL Node-A 通过 Gossip 协议向其他节点广播我觉得 B 可能挂了 半数以上主节点也认为 B 是 PFAIL → 集群标记 Node-B 为 FAIL坐实判定全集群广播4.2 故障转移完整过程Step 1: PFAIL 检测 Master-B 无响应超过 15 秒 → 多节点标记 PFAIL Step 2: FAIL 确认 超过半数主节点认同 → Master-B 标记 FAIL Step 3: 从节点选举 Master-B 的从节点们发起选举 Slave-1: 我是最老的从节点主从复制偏移量最大选我 Slave-2: 我 offset 小放弃 Step 4: Raft 投票 其他主节点投票 Slave-1 获得 majority过半数选票 → 当选为新 Master Step 5: 接管 新 Master 接管旧 Master 的 Slot 通过 Gossip 协议通知全集群Slot 0~5460 的新主人是 Slave-14.3 选举优先级Slave 竞选 Master 的优先级排序 1. repl_offset复制偏移量 → 数据最新的从节点优先最重要 2. cluster-slave-validity-factor × node-timeout → 与主断开时间太久的从节点自动弃权 3. slave-priority手动设置的优先级 → 数字越小优先级越高0 表示永不竞选 4. nodeId 字典序 → 以上全一样时nodeId 字母序小的当选确定性4.4 脑裂处理网络分区场景 原 Master 被隔离但自己不知道 集群选举出新 Master 原 Master 回归后 发现 Slot 已被接管 → 自动降级为 Slave 与新 Master 建立主从复制 → 丢弃自己的旧数据 结果短暂的脑裂后最终一致旧 Master 自己修复五、Spring Data Redis 配置spring:data:redis:# 单机host:localhostport:6379# 或 集群cluster:nodes:-127.0.0.1:7001-127.0.0.1:7002-127.0.0.1:7003-127.0.0.1:7004-127.0.0.1:7005-127.0.0.1:7006max-redirects:3# 或 哨兵sentinel:master:mymasternodes:-127.0.0.1:26379-127.0.0.1:26380-127.0.0.1:26381lettuce:pool:max-active:16max-idle:8min-idle:4timeout:3000msConfigurationpublicclassRedisConfig{BeanpublicRedisTemplateString,ObjectredisTemplate(RedisConnectionFactoryfactory){RedisTemplateString,ObjecttemplatenewRedisTemplate();template.setConnectionFactory(factory);// Jackson 序列化可读兼容性好Jackson2JsonRedisSerializerObjectserializernewJackson2JsonRedisSerializer(Object.class);template.setKeySerializer(RedisSerializer.string());template.setValueSerializer(serializer);template.setHashKeySerializer(RedisSerializer.string());template.setHashValueSerializer(serializer);returntemplate;}BeanpublicCacheManagercacheManager(RedisConnectionFactoryfactory){// Redis 作为 Spring Cache 的缓存后端returnRedisCacheManager.builder(factory).cacheDefaults(RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer()))).build();}}六、Jedis vs Lettuce vs RedissonJedisLettuceRedisson定位客户端客户端Spring 默认高级框架线程模型同步 连接池Netty 异步单连接复用Netty 异步集群支持✅✅✅分布式锁手写手写一行代码读写分离❌✅✅发布订阅✅✅✅最适合简单项目Spring Boot 默认需要锁/队列/限流Redisson 分布式锁一行代码RLocklockredissonClient.getLock(lock:order:orderId);// 自动续期看门狗不用担心业务超时锁被释放lock.lock();try{// 业务逻辑}finally{lock.unlock();}// 或 tryLock 带超时if(lock.tryLock(3,10,TimeUnit.SECONDS)){try{...}finally{lock.unlock();}}看门狗Watchdog机制 你 lock() 了但业务跑了太久 → Redisson 自动每 10 秒续期一次 → 业务完成 unlock()看门狗停止 → 永远不会出现锁过期被其他线程抢走的问题七、持久化RDB vs AOFRDBAOF全称Redis DatabaseAppend Only File原理定时快照整个内存记录每次写操作文件大小小压缩二进制大文本命令恢复速度快慢逐条回放数据丢失两次快照之间几乎不丢每秒 fsync适用备份、灾难恢复主持久化生产建议RDB AOF 双开 RDB 每 1 小时一次 AOF 每秒 fsync → 崩溃最多丢 1 秒数据 → 恢复时先加载 RDB 再回放 AOF八、内存淘汰策略当内存到达 maxmemory 时 noeviction — 不淘汰写操作直接报错默认不推荐 allkeys-lru — 所有 Key 中淘汰最近最少使用的推荐 allkeys-lfu — 所有 Key 中淘汰使用频率最低的 volatile-lru — 只淘汰设了过期时间的 Key volatile-ttl — 淘汰 TTL 最短的 缓存场景 → allkeys-lru 排行榜/计数 → noeviction不能丢数据九、缓存三大经典问题问题原因解决方案缓存穿透查不存在的数据每次都穿透到 DB布隆过滤器 / 空值缓存缓存击穿热点 Key 过期瞬间全部打到 DB互斥锁加载 / 永不过期 异步更新缓存雪崩大量 Key 同时过期 / Redis 宕机TTL 随机偏移 / 多级缓存 / 集群// 缓存击穿 — 互斥锁方案publicUsergetUser(Longid){Stringkeyuser:id;Usercached(User)redisTemplate.opsForValue().get(key);if(cached!null)returncached;// 只有一个线程去查 DBStringlockKeylock:user:id;BooleanlockedredisTemplate.opsForValue().setIfAbsent(lockKey,1,10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{UseruseruserMapper.selectById(id);redisTemplate.opsForValue().set(key,user,30,TimeUnit.MINUTES);returnuser;}finally{redisTemplate.delete(lockKey);}}else{Thread.sleep(100);returngetUser(id);// 重试}}十、总结数据类型 → 不止 KVList/Set/ZSet/Stream 覆盖 90% 的业务场景 集群选举 → Gossip 故障检测 Raft 投票 Slot 接管自动故障转移 三个节点 → 哈希槽分片 奇数共识 脑裂防护不是随意选的 Java 操作 → Spring Data Redis配置驱动 Redisson高级抽象