Chlx's Live

Back

Redis介绍#

键值对数据库 NoSql 数据库 非结构化 无关联 非sql BASE而非ACID

基于内存的数据库 Pasted image 20250928224332

基本操作#

登陆#

小坑:azure的云服务器除了在面板开放端口 还要检查安全组规则

redis-cli [options] [commonds]

通用命令#

Pasted image 20251001164629

Redis的Java客户端#

  • Jedis:同步阻塞,轻量简单,适合低并发,但高负载易线程问题。
  • Lettuce:基于 Netty 的异步非阻塞,支持响应式、高弹性、集群功能,适用于高吞吐生产环境。新项目推荐 Lettuce。
  • Redisson:是在Redis基础上实现了分布式的可伸缩的java数据结构,例如Map、Queue等,而且支持跨进程的同步机制。

Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式

SpringDataRedis框架#

RedisTemplate 是 Spring Data Redis 模块提供的一个核心类,它封装了对 Redis 数据操作的底层细节

集成了序列化/反序列化机制: Redis template的序列化器默认是采用JDK序列化 可读性差、占内存:它会将 Java 对象连同其完整的类路径、版本号、字段元数据等大量信息,存进 Redis 的是一堆带有 Java 类元数据的二进制流

两种序列化优化方案:#
  1. 自定义redis template,RedisConfig中配置序列化器
  2. String序列化器StringRedisTemplate 对象需要手动序列化

JSON 序列化器(例如 Jackson2JsonRedisSerializer)的工作就是: 自动把你内存里的 Java 对象,转换成 JSON 格式的字符串表示,然后再存进 Redis。

为了节省内存空间,通用性和解耦,我们不使用JSON序列化器来处理value,而是统一使用String序列化器(放弃框架的“自动魔法”,选择一种更明确、更可控、更通用的手动模式)。只能存储String类型的key和value 当需要存储Java对象时,手动完成对象的序列化和反序列化

这种用法比较普遍,因此SpringDataRedis就提供了RedisTemplate的子类:StringRedisTemplate,它的key和value的序列化方式默认就是String方式

它在底层强制绑定了 StringRedisSerializer。这个序列化器干的事情非常基础:就是把 Java 的 String 按照 UTF-8 编码翻译成底层的 byte[](字节数组)存进 Redis,取出来时再把 byte[] 翻译回 String

分布式缓存#

单点redis的问题 : 数据丢失、并发、故障恢复、存储上限 Pasted image 20251012165644

redis持久化#

RDB 数据快照#

执行了 save 命令,就会在主线程生成 RDB 文件,会阻塞主线程; 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

RDB 文件的加载工作是在服务器启动时自动执行 优雅关闭会触发最后的 SAVE 或 AOF 同步

自动触发BGSAVE的机制可以在redis.config自定义 bgsave是异步的 但在fork主进程时会阻塞: fork主进程得到子进程,就是复制页表 copy-on-write/写时复制技术 主进程写操作时 则会在内存里面复制一份新的副本对副本进行写操作(导致内存膨胀) 子进程读共享内存空间写新RDB替换旧RDB

AOF(Append Only File)#

记录每一个写命令 默认是关闭

Pasted image 20251012195234 也会进行编码压缩处理 bgrewriteaof是异步的

三种刷盘机制 默认先缓存区1s后落盘

触发阈值(绝对体积,相对增长)自动重写aof Pasted image 20251012201055 安全性高、宕机恢复速度较慢

Redis 主从集群#

互联网业务场景中,读操作的频率远大于写操作,即所谓的读多写少

最核心目的是 读写分离

配置主从#

临时replicaof <masterip> <masterport> ;永久的在conf中配置

主从的数据同步原理#

第一次同步一定是全量同步 执行bgsave然后网络发送RDB文件 耗时!

完整流程描述:

  • slave节点请求增量同步
  • master节点判断replid,发现不一致,拒绝增量同步
  • master将完整内存数据生成RDB,发送RDB到slave
  • slave清空本地数据,加载master的RDB
  • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
  • slave执行接收到的命令,保持与master之间的同步

主从断线重连时(利用 backlog 补发)

增量同步 短暂断连后,从节点会告知主节点自己最后同步的offset,主节点随即只把断连期间缺失的写命令发送过去,从而避免了昂贵的全量数据复制

repl_backlog是固定大小的环形数组

slave断开太久 repl_backlog的offset已经覆盖 只能执行全量同步

主从同步优化#

  • 在master中配置repl-diskless-sync yes启用无磁盘复制直接网络发送,避免全量同步时的磁盘IO。
  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
  • 适当提高repl_baklog的大小,尽可能避免全量同步
  • 主-从-从链式结构,减少master压力

哨兵节点#

解决master宕机 哨兵集群功能:检测集群状态 故障恢复主从切换 通知clients

原理#

心跳机制监测状态 ping超过1s没回复 认为该实例主观下线 超过指定数量的哨兵都认为实例主观下线 则是客观下线 主从切换 Pasted image 20251012213429 就是挑offset值小的 选举领头 Sentinel,领头的才有权执行故障转移操作 故障转移: salveof no one让其成为master 然后广播让其他从认新的主(包括曾经的master)

Pasted image 20251012215145

搭建#

配置sentinel.conf: sentinel monitor <master-name> <ip> <port> <quorum>

yml配置哨兵集群的地址 Pasted image 20251012215413

Redis分片集群#

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题
  • 高并发写的问题

分片集群特征:

  • 集群中有多个master,每个master保存不同数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

搭建分片集群#

配置好redis.conf文件 redis-cli —cluster 启动

自动路由

散列插槽#

key不是与节点绑定,而是与插槽绑定,无论你的集群如何变化,这个绑定关系永远不变。 只是一个逻辑地址,中间寻址层,目的是分类

Pasted image 20251012221649 {}里面的是有效部分

集群伸缩#

启动一个全新的、以 cluster 模式运行的 Redis 实例 add-node 默认新增的是master 重分片 使用 redis-cli --cluster reshard 命令启动一个交互式的重分片计划

故障转移#

master宕机 自动故障转移

手动故障转移 Pasted image 20251012225655

java客户端访问分片集群#

数据结构#

数据结构#

动态字符串SDS#

c语言的字符数组缺点:

Pasted image 20251014214608 Pasted image 20251014173226 SDS优点:获取长度O(1),动态扩容,减少内存分配次数,二进制安全(数据中出现值为0的字节被认为C字符串结束符)

IntSet#

唯一、有序、可变 Pasted image 20251014174023 Pasted image 20251014174220 contents的作用只是个指针,一个起始地址

固定encoding 方便指针寻址

动态升级,节省空间:添加数字超过了原来的编码范围,自动升级编码方式扩容(原来的元素倒序遍历拷贝到扩容后新位置) 显然,导致升级的新元素要么插入队首要么队尾

有序性 底层二分查找插入,故数据量不要太多

Dict#

键值映射的实现 Pasted image 20251014192629哈希表的大小只能是2^n 默认4 数组结合链表 dictEntry使用指针 内存碎片化

触发哈希表扩容的两种情况:

  1. 负载因子used/size >=1 且无bgsave或者bgrewrite 等后台进程
  2. LoadFactor >5 dictExpand函数扩容到第一个大于等于entry个数+1的2^n

触发哈希表收缩: LoadFactor < 0.1 dictExpand函数调整大小

哈希表size变化需要创建新哈希表 因此需要对每个key重新计算索引,称作rehash 预防阻塞 是渐进式rehash Pasted image 20251014194307

ZipList 压缩链表#

Pasted image 20251014195133 Pasted image 20251014195517

Pasted image 20251014195742 一个十六进制数表示4 bit

ziplistentry的encoding 3+1种

ziplist的连锁更新问题: 记录前一字节的长度的属性从1变5字节可能会导致后面的entry的属性也变化 Pasted image 20251014203943 为了解决这个问题 listpack

QuickList#

zip申请的内存连续 如果过多申请效率变低

创建多个ziplist 分片存储 Pasted image 20251014204407 Pasted image 20251014204445 默认值是-2 不超过8kb 还可选压缩深度 ,默认1

Pasted image 20251014204842

SkipList#

◆跳跃表是一个双向链表,每节点都包含score(排序用)和ele值(sds)

◆节点按照score值排序,score值一样则按照ele字典排序

◆每个节点都可以包含多层指针,层数是1到32之间

◆不同层指针到下一个节点的跨度不同,层级越高,跨度越大

◆增删改查效率与红黑树基本一致,实现却更简单

RedisObject#

Pasted image 20251014205950

robj头16字节

五种数据类型:#

Pasted image 20251014210329

String#

基本编码是RAW 通过SDS实现

44+SDS头的3字节+sds尾部终止符1字节+robj头的16字节=64 bytes ,利于GM-Lock 内存分配 所以 SDS长度小于44 编码embstr 此时object head和sds是一段连续的内存空间

如果存储的字符串是整数值,并且大小在LONG_MAX范围内,则会采用INT编码:直接将数据保存在RedisObject的 ptr指针位置(刚好8字节),不再需要SDS了。 Pasted image 20251014215322

List#

就是redis object头 + quicklist Pasted image 20251014221038

Set#

Set 类型的底层数据结构是由哈希表或整数集合实现的:

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。value都是null

ZSet#

要求 可排序 键值查询

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构; Pasted image 20251014225359
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构,如图 Pasted image 20251014224818

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

Hash#

网络模型#

用户空间和内核空间 为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
1.进程的寻址空间会划分为两部分:内核空间、用户空间
2.用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
3.内核空间可以执行特权命令(Ring0),调用一切系统资源

Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
1.写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
2.读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

5种IO模型:#

Pasted image 20251015204603

阻塞IO#

Pasted image 20251015181956 两个阶段都是阻塞状态

非阻塞IO#

非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程 Pasted image 20251015182109 用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能无提高甚至下降。而且忙等机制会导致CPU空转

IO多路复用#

文件描述符(File Descriptor):简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件 IO多路复用:利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知 监听FD的方式、通知的方式又有多种实现,常见的有:

select模式存在的三个问题:
1.能监听的FD最大不超过1024
2.每次select都需要把所有要监听的FD都拷贝到内核空间
3.每次都要遍历所有FD来判断就绪状态

poll模式的问题:
1.poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降

epoll模式中如何解决这些问题的?
1.基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
2.每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间 3.利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降

事件通知机制: 当FD有数据可读时,我们调用epoll_wait(或者select、poll)可以得到通知。但是事件通知的模式有两种:
LevelTriggered:简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。
EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。 ET模式避免了LT模式可能出现的惊群现象
ET模式最好结合非阻塞IO读取FD数据,相比LT会复杂一些 Pasted image 20251015203807

信号驱动IO及异步IO#

Pasted image 20251015204133是真的非阻塞 Pasted image 20251015204302 异步IO模型中,用户进程在两个阶段都是非阻塞状态。 缺点都是 并发性能一般 有风险

Redis 网络模型:#

为什么命令处理是’‘单线程‘’ 内存够快 CPU 不是瓶颈,多线程意义不大,而且需要考虑上下文切换和加锁的开销

Redis 6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。

Pasted image 20251015215251

RESP协议#

Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub): 客户端(client)向服务端(server)发送一条命令 服务端解析并执行命令,返回响应结果给客户端

内存回收#

过期key处理#

redis是怎么判断TTL到期 redisdatabase结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL Pasted image 20251015225706 键值对字典 的key和value 都是redisobj 过期字典 里面:

  • key: 与 dict 字典中的 key 是同一个指针,不会重复创建,节省空间
  • value: 是一个 long long 类型的时间戳,表示键过期的毫秒精度 UNIX 时间戳

过期key的删除策略:合理使用 CPU 时间和避免内存浪费之间取得平衡

  1. 惰性清理:每次访问key时判断是否过期,如果过期则删除
  2. 定期清理:定期抽20个(逐个遍历db里面的bucket)过期字典里面的key,判断是否过期,如果过期则删除。 定期清理的两种模式(核心的清理逻辑和算法是共享的): 触发时机/频率和最大耗时限制 区别: SLOW模式,初始化启动定时任务,按照server.hz执行频率默认为10 hz,最大耗时25ms,避免了循环过度 FAST模式,执行频率受beforeSleep()调用频率影响,间隔限制2ms,最大耗时1ms Pasted image 20251015231709

内存淘汰策略#

执行时机: Pasted image 20251015232315 八策略 Pasted image 20251015232420 TTL 送你一程 RANDOM 顾名思义 LRU 最少最近使用 淘汰最久未使用的键值 LFU 最少频率使用 淘汰最少使用的键值 配置的策略不同 redisobj头里面的存储信息不同Pasted image 20251015232925

淘汰流程 Pasted image 20251015233856

LRU TTL LFU 策略 肯定不是遍历全部然后排序啊 故部分采样

Redis最佳实践#

Redis键值设计#

优雅设计#

  • [业务名称]:[数据名]:[id]
  • 长度不超过44字节bytes

key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,还有可能产生内存碎片

NO bigkeys#

指这个 Key 所对应的 Value 所占用的内存空间过大,或者其包含的成员数量过多

一般而言,下面这两种情况被称为大 key:

  • String 类型的值大于 10 KB;
  • Hash、List、Set、ZSet 类型的元素的个数超过 5000个;

造成的问题:

  • 客户端超时阻塞。由于Redis执行命令是单线程处理,然后在操作大key时会比较耗时,那么就会阻塞Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。少量的QPS就可能导致带宽使用率被占满
  • 阻塞工作线程。如果使用del删除大key时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 数据倾斜/内存分布不均。集群模型在slot分片均匀情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS也会比较大。
  • 集群扩展与运维困难 扩缩容阻塞、主从同步延迟

redis-cli —bigkeys 查找大key 遍历keys返回每种类型中最大的那个 bigkey 对于集合类型来说,这个方法只统计集合元素个数的多少 会阻塞 建议在从节点执行 使用 SCAN 命令查找大 key 结合 SCAN + TYPE 结合 STRLEN / LLEN / HLEN / SCARD / ZCARD 这些 O(1) 复杂度的命令 SCAN + MEMORY USAGE 的cpu开销大 不建议  第三方监控工具  如RdbTools 工具查找大 key  网络监控

如何删除BigKey#

删除大key耗时 导致阻塞

  • 分批次删除
  • 异步删除 unlink(Redis 4.0版本以上)

合适的数据类型#

hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多 可以调整上限 但是entry过多就会导致BigKey问题

太多entry的hash”打散“分片(no bigkey):基于字段哈希值的固定分片

批处理优化#

多个命令执行时的网络等待 Pasted image 20251013221305Pasted image 20251013221316

Mxxx的命令批量插入数据

Pipeline 支持任意命令 管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能

注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞

集群下的批处理#

一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败 Pasted image 20251013222248

spring封装的是并行 Slot 性能已经非常好了

服务端优化#

持久化配置#

Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:

  • 用来做缓存的Redis实例尽量不要开启持久化功能
  • 建议关闭RDB持久化功能,使用AOF持久化
  • 利用脚本定期在slave节点做RDB,实现数据备份
  • 设置合理的rewrite阈值,避免频繁的bgrewrite
  • 配置no-appendfsync-on-rewrite = yes,禁止在rewrite和rdb fork期间做aof,避免因AOF引起的阻塞
  • 部署有关建议:
    • Redis实例的物理机要预留足够内存,应对fork和rewrite
    • 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
    • 不要与CPU密集型应用部署在一起
    • 不要与高硬盘负载应用一起部署。例如:数据库、消息队列

慢查询优化#

  1. 发送命令;
  2. 命令排队;
  3. 命令执行;
  4. 返回结果。

查看慢查询日志列表:

  • slowlog len:查询慢查询日志长度
  • slowlog get [n]:读取n条慢查询日志
  • slowlog reset:清空慢查询列表

Redis内存划分和内存配置#

Pasted image 20251013233126 内存碎片 不会影响 Redis 性能,但是会增加内存消耗

我们在使用redis过程中,处理大量的big value,那么会导致我们的输出结果过多,如果输出缓存区过大,会导致redis直接断开,而默认配置的情况下, 其实他是没有大小的,内存可能一下子被占满,会直接导致咱们的redis断开,所以解决方案有两个 1、设置一个大小 2、增加我们带宽的大小

集群的缺点#

在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务: 可配置取消

集群带宽问题:相互ping 集群状态信息和slots信息等,节点多了数据量也越大,导致带宽需求高。 拆分集群,合适的cluser-node-time值(ping频率相关),

命令的集群兼容性问题

lua和事务的问题 lua和事务都是要保证原子性问题,如果你的key不在一个节点,那么是无法保证lua的执行和事务的特性的,所以在集群模式是没有办法执行lua和事务的

Redis 黑马
https://lixuan.live/blog/redis-hei-ma
Author Chlx
Published at 2025年10月24日
Comment seems to stuck. Try to refresh?✨