记录redis学习
Redis — 从数据结构到集群原理
一、为什么是 Redis
Redis(REmoteDIctionaryServer)是一个基于内存的单线程高性能 KV 数据库。
核心指标: 读写速度:10W+ QPS / 单节点 操作原子:单线程 + 事件驱动 → 天然线程安全 持久化: RDB(快照)+ AOF(日志)双保险| 对比 | Redis | MySQL | MongoDB |
|---|---|---|---|
| 存储 | 内存 | 磁盘 | 磁盘 |
| QPS | 10W+ | 几千 | 几千 |
| 数据结构 | KV + List/Set/Hash/ZSet… | 表 | 文档 |
| 持久化 | RDB + AOF | 原生 | 原生 |
| 典型用途 | 缓存/队列/计数/分布式锁 | 核心业务数据 | 日志/JSON 存储 |
二、基础数据类型与使用场景
Redis 比你想象的更强大 — 它不是简单的 KV,而是"数据结构服务器"| 类型 | 底层结构 | 典型应用 |
|---|---|---|
| String | SDS(动态字符串) | 缓存、计数器、分布式锁 |
| Hash | Dict / ListPack | 用户信息、购物车 |
| List | QuickList | 消息队列、最新列表 |
| Set | HT / ListPack | 标签、共同好友 |
| ZSet | SkipList + HT | 排行榜、延迟队列 |
| Stream | RadixTree | 可靠消息队列(类似 Kafka) |
| Bitmap | String 位操作 | 签到、布隆过滤器 |
| HyperLogLog | 概率算法 | UV 统计(12KB 估亿级) |
| Geo | ZSet 封装 | LBS、附近的人 |
2.1 String — 万物皆可存
命令:GET / SET / INCR / SETNX / SETEX 底层:SDS(Simple Dynamic String),O(1) 取长度,不会溢出// 缓存stringRedisTemplate.opsForValue().set("user:1001",json,30,TimeUnit.MINUTES);// 计数器Longcount=stringRedisTemplate.opsForValue().increment("pv:article:123");// 分布式锁Booleanlocked=stringRedisTemplate.opsForValue().setIfAbsent("lock:order:"+orderId,"1",10,TimeUnit.SECONDS);2.2 Hash — 对象存储
命令:HSET / HGET / HGETALL / HINCRBY 底层:Dict(默认)+ ListPack(小数据量自动切换,省内存)// 存储用户信息Map<String,String>user=Map.of("name","张三","age","28","city","北京");stringRedisTemplate.opsForHash().putAll("user:1001",user);// 只更新某个字段stringRedisTemplate.opsForHash().increment("user:1001","age",1);对比 String 存 JSON:Hash 可以原子更新单个字段,不用读 → 改 → 写。
2.3 List — 有序队列
命令:LPUSH / RPUSH / LPOP / RPOP / LRANGE / BLPOP 底层:QuickList(Linked List + ListPack 混合,省内存)// 消息队列 — 生产者stringRedisTemplate.opsForList().leftPush("order:queue",orderJson);// 消费者 — 阻塞等待(60 秒超时)Stringmsg=stringRedisTemplate.opsForList().rightPop("order:queue",60,TimeUnit.SECONDS);// 最新 10 条动态List<String>latest=stringRedisTemplate.opsForList().range("timeline:user:1001",0,9);2.4 Set — 无序去重集合
命令:SADD / SREM / SINTER / SUNION / SDIFF 底层:Dict(大 Set)+ ListPack(小 Set)// 共同好友Set<String>commonFriends=stringRedisTemplate.opsForSet().intersect("friends:user:1","friends:user:2");// 你可能认识的人(差集)Set<String>suggestions=stringRedisTemplate.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 名(倒序)Set<ZSetOperations.TypedTuple<String>>top10=stringRedisTemplate.opsForZSet().reverseRangeWithScores("leaderboard",0,9);// 延迟队列 — 取到期的任务Set<String>tasks=stringRedisTemplate.opsForZSet().rangeByScore("delay:queue",0,System.currentTimeMillis());2.6 Stream — 可靠消息队列
消息队列演进: List BLPOP → 无 ACK,消费者崩溃消息丢失 Pub/Sub → 无持久化,断连就丢消息 Stream → 有 ACK + 持久化 + 消费者组(≈ 轻量级 Kafka)// 发送消息Map<String,String>msg=Map.of("orderId","12345","amount","99.9");stringRedisTemplate.opsForStream().add("order:stream",msg);// 消费者组消费List<MapRecord<String,Object,Object>>records=stringRedisTemplate.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 操作 |
|---|---|---|
| Bitmap | SETBIT / GETBIT / BITCOUNT | opsForValue().setBit() |
| HyperLogLog | PFADD / PFCOUNT | opsForHyperLogLog().add() |
| Geo | GEOADD / GEORADIUS | opsForGeo().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:3000ms@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(factory);// Jackson 序列化(可读,兼容性好)Jackson2JsonRedisSerializer<Object>serializer=newJackson2JsonRedisSerializer<>(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 Redisson
| Jedis | Lettuce | Redisson | |
|---|---|---|---|
| 定位 | 客户端 | 客户端(Spring 默认) | 高级框架 |
| 线程模型 | 同步 + 连接池 | Netty 异步(单连接复用) | Netty 异步 |
| 集群支持 | ✅ | ✅ | ✅ |
| 分布式锁 | 手写 | 手写 | 一行代码 |
| 读写分离 | ❌ | ✅ | ✅ |
| 发布订阅 | ✅ | ✅ | ✅ |
| 最适合 | 简单项目 | Spring Boot 默认 | 需要锁/队列/限流 |
Redisson 分布式锁(一行代码)
RLocklock=redissonClient.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 AOF
| RDB | AOF | |
|---|---|---|
| 全称 | Redis Database | Append 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){Stringkey="user:"+id;Usercached=(User)redisTemplate.opsForValue().get(key);if(cached!=null)returncached;// 只有一个线程去查 DBStringlockKey="lock:user:"+id;Booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",10,TimeUnit.SECONDS);if(Boolean.TRUE.equals(locked)){try{Useruser=userMapper.selectById(id);redisTemplate.opsForValue().set(key,user,30,TimeUnit.MINUTES);returnuser;}finally{redisTemplate.delete(lockKey);}}else{Thread.sleep(100);returngetUser(id);// 重试}}十、总结
数据类型 → 不止 KV,List/Set/ZSet/Stream 覆盖 90% 的业务场景 集群选举 → Gossip 故障检测 + Raft 投票 + Slot 接管,自动故障转移 三个节点 → 哈希槽分片 + 奇数共识 + 脑裂防护,不是随意选的 Java 操作 → Spring Data Redis(配置驱动)+ Redisson(高级抽象)