当前位置: 首页 > news >正文

记录redis学习

Redis — 从数据结构到集群原理


一、为什么是 Redis

Redis(REmoteDIctionaryServer)是一个基于内存的单线程高性能 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可靠消息队列(类似 Kafka)
BitmapString 位操作签到、布隆过滤器
HyperLogLog概率算法UV 统计(12KB 估亿级)
GeoZSet 封装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 操作
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-1

4.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

JedisLettuceRedisson
定位客户端客户端(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

RDBAOF
全称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){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(高级抽象)
http://www.jsqmd.com/news/1096500/

相关文章:

  • 别再硬编码密钥了!Spring Boot项目实战:用配置文件安全管理AES256加解密密钥
  • 大模型 AGI 开发模式:从概念到落地的系统性技术解构
  • STC16F40K128单片机驱动4路红外循迹模块实战指南
  • HarmonyOS7 泛型组件怎么写才不废?TypeScript 类型安全通用列表实战
  • 终极指南:如何用Python免费下载B站大会员4K高清视频
  • 网络基础入门与实战操作指南
  • 终极指南:如何用MPC-HC打造专业级Windows媒体播放体验 [特殊字符]
  • 一键下载中小学电子课本:国家中小学智慧教育平台PDF下载工具完全指南
  • 海量简历筛选太痛苦?实测AI智能体批量归档黑科技,猎头效能提升10倍
  • 解锁B站缓存视频:m4s-converter工具完整使用指南
  • 同步与异步通信:从概念到实战,如何为你的系统选择最佳通信模式?
  • 进口气动三通调节阀:工业流体合/分流控制怎么选-米勒阀门
  • 从“AI辅助”到“AI协同”:一线大厂已上线的代码生成可信度分级标准(含自动校验插件开源地址)
  • PaddleOCR和Tesseract识别中英文对比
  • 想淘伯爵possession?先看看这处表壳加工公差再决定
  • 在openEuler 22.03 LTS上实战部署Docker:从源配置到避坑指南
  • STM32F103C8T6矩阵键盘驱动:从扫描法到中断优化的实战解析
  • 攻防拐点:从“发现漏洞”到“机器速度修复”,解构 OpenAI 的网络安全新野心
  • HarmonyOS7 虚拟列表不卡顿的关键在哪?动态高度和多列布局这样封装
  • 多通道高速采集的DDR瓶颈:你以为带宽够,其实差一个数量级
  • 面试官最爱问的流水线反压问题,我用这个Verilog握手模块搞定了
  • QY-18A、QY-18B、QY-18DL 和 QY-18DL-1 四种倾斜位移监测设备的参数对比及优劣
  • 群晖SSL证书:申请+部署+续期
  • LLM代码生成准确率已达89.7%(IEEE TSE 2024最新基准),但93%项目仍因这4个隐性缺陷失败
  • CoAP协议实战:从报文解析到工具链应用
  • 【技术解析】基于卷积神经网络的图像风格迁移:从Gatys经典算法到实践应用
  • 终极指南:3个实战场景带你玩转OpenXLSX C++ Excel库
  • 从“水泥地”到“镜面地”——地坪如何改变车间面貌
  • Flowable UI实战:从零绘制一个BPMN标准请假审批流程图
  • Flux、Mono、Reactor 核心操作符与高阶应用场景深度解析