Redis介绍#
键值对数据库 NoSql 数据库 非结构化 无关联 非sql BASE而非ACID
基于内存的数据库

基本操作#
登陆#
小坑:azure的云服务器除了在面板开放端口 还要检查安全组规则
redis-cli [options] [commonds]
通用命令#

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 类元数据的二进制流
两种序列化优化方案:#
- 自定义redis template,RedisConfig中配置序列化器
- 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的问题 : 数据丢失、并发、故障恢复、存储上限

redis持久化#
RDB 数据快照#
执行了 save 命令,就会在主线程生成 RDB 文件,会阻塞主线程; 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞
RDB 文件的加载工作是在服务器启动时自动执行
优雅关闭会触发最后的 SAVE 或 AOF 同步
自动触发BGSAVE的机制可以在redis.config自定义 bgsave是异步的 但在fork主进程时会阻塞: fork主进程得到子进程,就是复制页表 copy-on-write/写时复制技术 主进程写操作时 则会在内存里面复制一份新的副本对副本进行写操作(导致内存膨胀) 子进程读共享内存空间写新RDB替换旧RDB
AOF(Append Only File)#
记录每一个写命令 默认是关闭
也会进行编码压缩处理
bgrewriteaof是异步的
三种刷盘机制 默认先缓存区1s后落盘
触发阈值(绝对体积,相对增长)自动重写aof
安全性高、宕机恢复速度较慢
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没回复 认为该实例主观下线
超过指定数量的哨兵都认为实例主观下线 则是客观下线
主从切换
就是挑offset值小的
选举领头 Sentinel,领头的才有权执行故障转移操作
故障转移:
salveof no one让其成为master
然后广播让其他从认新的主(包括曾经的master)

搭建#
配置sentinel.conf:
sentinel monitor <master-name> <ip> <port> <quorum>
yml配置哨兵集群的地址

Redis分片集群#
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
- 海量数据存储问题
- 高并发写的问题
分片集群特征:
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
搭建分片集群#
配置好redis.conf文件 redis-cli —cluster 启动
自动路由
散列插槽#
key不是与节点绑定,而是与插槽绑定,无论你的集群如何变化,这个绑定关系永远不变。 只是一个逻辑地址,中间寻址层,目的是分类
{}里面的是有效部分
集群伸缩#
启动一个全新的、以 cluster 模式运行的 Redis 实例
add-node 默认新增的是master
重分片
使用 redis-cli --cluster reshard 命令启动一个交互式的重分片计划
故障转移#
master宕机 自动故障转移
手动故障转移

java客户端访问分片集群#
数据结构#
数据结构#
动态字符串SDS#
c语言的字符数组缺点:
SDS优点:获取长度O(1),动态扩容,减少内存分配次数,二进制安全(数据中出现值为0的字节被认为C字符串结束符)
IntSet#
唯一、有序、可变
contents的作用只是个指针,一个起始地址
固定encoding 方便指针寻址
动态升级,节省空间:添加数字超过了原来的编码范围,自动升级编码方式扩容(原来的元素倒序遍历拷贝到扩容后新位置) 显然,导致升级的新元素要么插入队首要么队尾
有序性 底层二分查找插入,故数据量不要太多
Dict#
键值映射的实现
哈希表的大小只能是2^n 默认4
数组结合链表 dictEntry使用指针 内存碎片化
触发哈希表扩容的两种情况:
- 负载因子used/size >=1 且无bgsave或者bgrewrite 等后台进程
- LoadFactor >5 dictExpand函数扩容到第一个大于等于entry个数+1的2^n
触发哈希表收缩: LoadFactor < 0.1 dictExpand函数调整大小
哈希表size变化需要创建新哈希表 因此需要对每个key重新计算索引,称作rehash
预防阻塞 是渐进式rehash

ZipList 压缩链表#

一个十六进制数表示4 bit
ziplistentry的encoding 3+1种
ziplist的连锁更新问题: 记录前一字节的长度的属性从1变5字节可能会导致后面的entry的属性也变化
为了解决这个问题 listpack
QuickList#
zip申请的内存连续 如果过多申请效率变低
创建多个ziplist 分片存储
默认值是-2 不超过8kb
还可选压缩深度 ,默认1

SkipList#
◆跳跃表是一个双向链表,每节点都包含score(排序用)和ele值(sds)
◆节点按照score值排序,score值一样则按照ele字典排序
◆每个节点都可以包含多层指针,层数是1到32之间
◆不同层指针到下一个节点的跨度不同,层级越高,跨度越大
◆增删改查效率与红黑树基本一致,实现却更简单
RedisObject#

robj头16字节
五种数据类型:#

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了。

List#
就是redis object头 + quicklist

Set#
Set 类型的底层数据结构是由哈希表或整数集合实现的:
- 如果集合中的元素都是整数且元素个数小于
512(默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构; - 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。value都是null
ZSet#
要求 可排序 键值查询
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
- 如果有序集合的元素个数小于
128个,并且每个元素的值小于64字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构,如图

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了
Hash#
网络模型#
用户空间和内核空间
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
1.进程的寻址空间会划分为两部分:内核空间、用户空间
2.用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
3.内核空间可以执行特权命令(Ring0),调用一切系统资源
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
1.写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
2.读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
5种IO模型:#

阻塞IO#
两个阶段都是阻塞状态
非阻塞IO#
非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程
用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能无提高甚至下降。而且忙等机制会导致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会复杂一些

信号驱动IO及异步IO#
是真的非阻塞
异步IO模型中,用户进程在两个阶段都是非阻塞状态。
缺点都是 并发性能一般 有风险
Redis 网络模型:#
为什么命令处理是’‘单线程‘’ 内存够快 CPU 不是瓶颈,多线程意义不大,而且需要考虑上下文切换和加锁的开销
Redis 6.0版本中引入了多线程,目的是为了提高IO读写效率。因此在解析客户端命令、写响应结果时采用了多线程。核心的命令执行、IO多路复用模块依然是由主线程执行。

RESP协议#
Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub): 客户端(client)向服务端(server)发送一条命令 服务端解析并执行命令,返回响应结果给客户端
内存回收#
过期key处理#
redis是怎么判断TTL到期
redisdatabase结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL
键值对字典 的key和value 都是redisobj
过期字典 里面:
key: 与dict字典中的 key 是同一个指针,不会重复创建,节省空间value: 是一个long long类型的时间戳,表示键过期的毫秒精度 UNIX 时间戳
过期key的删除策略:合理使用 CPU 时间和避免内存浪费之间取得平衡
- 惰性清理:每次访问key时判断是否过期,如果过期则删除
- 定期清理:定期抽20个(逐个遍历db里面的bucket)过期字典里面的key,判断是否过期,如果过期则删除。
定期清理的两种模式(核心的清理逻辑和算法是共享的):
触发时机/频率和最大耗时限制 区别:
SLOW模式,初始化启动定时任务,按照server.hz执行频率默认为10 hz,最大耗时25ms,避免了循环过度
FAST模式,执行频率受beforeSleep()调用频率影响,间隔限制2ms,最大耗时1ms

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

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):基于字段哈希值的固定分片
批处理优化#
多个命令执行时的网络等待


Mxxx的命令批量插入数据
Pipeline 支持任意命令 管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能
注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞
集群下的批处理#
一次请求中携带多条命令,而此时如果Redis是一个集群,那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败

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密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
慢查询优化#
- 发送命令;
- 命令排队;
- 命令执行;
- 返回结果。
查看慢查询日志列表:
- slowlog len:查询慢查询日志长度
slowlog get [n]:读取n条慢查询日志- slowlog reset:清空慢查询列表
Redis内存划分和内存配置#
内存碎片 不会影响 Redis 性能,但是会增加内存消耗
我们在使用redis过程中,处理大量的big value,那么会导致我们的输出结果过多,如果输出缓存区过大,会导致redis直接断开,而默认配置的情况下, 其实他是没有大小的,内存可能一下子被占满,会直接导致咱们的redis断开,所以解决方案有两个 1、设置一个大小 2、增加我们带宽的大小
集群的缺点#
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务: 可配置取消
集群带宽问题:相互ping 集群状态信息和slots信息等,节点多了数据量也越大,导致带宽需求高。 拆分集群,合适的cluser-node-time值(ping频率相关),
命令的集群兼容性问题
lua和事务的问题 lua和事务都是要保证原子性问题,如果你的key不在一个节点,那么是无法保证lua的执行和事务的特性的,所以在集群模式是没有办法执行lua和事务的