redis思维导图
##redis应用
###redis分布式锁
分布式锁
分布式应用进行逻辑处理时经常会遇到并发问题
读取和保存状\n态这两个操作不是原子的
set($key, $random, ['nx', 'ex' => 3])
使用lua脚本保证原子性if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
超时问题
Redis 分布式锁不要用于较长时间的任务
set 指令的 value 参数设置为一个随机数,释放锁时先匹配\n随机数是否一致,然后再删除 key
Lua 脚本可以保证连续多个指令的原子性执行
可重入性
可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加\n锁,那么这个锁就是可重入的
不推荐使用可重入锁,它加重了程序的复杂性
延时队列
异步消息队列
Redis的list(列表)数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,\n使用 lpop 和 rpop 来出队列
队列空了怎么办?
如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop
空轮询不但拉高了客户端的 CPU, redis 的 QPS 也\n会被拉高
使用sleep来解决问题,让线程睡一会。客户端的CPU和Redis的QPS都降下来了
队列延迟
blpop/brpop这两个指令的前缀字符 b 代表的是 blocking,也就是阻塞读
阻塞读在队列没有数据时进入休眠状态,数据到来则立刻醒来。消息延迟几乎为零。用 blpop/brpop 替代前面的 lpop/rpop
延时队列的实现
延时队列可以通过 Redis 的 zset(有序列表) 来实现
将消息序列化成一个字符串作\n为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期\n的任务进行处理,多个线程是为了保障可用性,万一挂了一个线程还有其它线程可以继续处\n理
Redis 的 zrem 方法是多线程多进程争抢任务的关键
位图
介绍
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组
基本使用
Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自\n动将位数组进行零扩充
setbit 零存整取
统计和查找
位图统计指令 bitcount 和位图查找指令 bitpos
hypeloglog
使用方法
HyperLogLog 数据结构是 Redis 的高级数据结构,非常有用,但是用的人很少
HyperLogLog 提供了两个指令 pfadd 和 pfcount,添加和统计
pfmerge 适合什么场合用
指令 pfmerge,用于\n将多个 pf 计数值累加在一起形成一个新的 pf 值
HyperLogLog 实现原理
Redis 中的 HyperLogLog 一共分了2^14=16384个桶,每个桶占 6 个 bit
一个数据,塞入 HyperLogLog 之前,先 hash 一下,得到一个 64 位的二进制数据
布隆过滤器(redis插件)
布隆过滤器是什么?
布隆过滤器可以理解为不怎么精确的 set 结构,当使用contains方法判断某个对象是否存在时,它可能会误判
参数设置合理,精确度可以控制的相对足够精确
布隆过滤器相对于Set、Map 等数据结构来说,它可以更高效地插入和查询,并且占用空间更少
Redis中的布隆过滤器
布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式启用
布隆过滤\n器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能
布隆过滤器基本使用
bf.add 添加元素, bf.exists 查询元素是否存在
bf.madd 批量添加元素, bf.mexists 批量查询元素是否存在
布隆过滤器的原理
每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无\n偏 hash 函数
一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个
简单限流
如何使用 Redis 来实现简单限流策略?
指定客户端的某个行为 action_key 在特定的时间内 period 只允许发生一定的次数
添加计数器,用redis判断是否满足
解决方案
限流需求中存在滑动时间窗口,用zset 数据结构的 score 值,可以通过 score 来圈出这个时间窗口来
Redis 操作都是针对同一个 key 的,使用 pipeline 可以显著提升 Redis 存取效率
漏桶限流(redis的插件-redis-cell)
常用的限流算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求
redis-cell
Redis 4.0提供了限流 Redis 模块,它叫 redis-cell
该模块也使用了漏斗算法,并提供了原子的限流指令
指令
cl.throttle loudou 15 30 60 1
15 这是漏斗容量\n30 /60 这是漏斗速率\n1 可选参数 默认是1
geohash 定位
用数据库来算附近的人
Redis 在 3.2 版本以后增加了地理位置 GEO 模块,可以使用 Redis 来实现附近的人
地图元素的位置数据使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90,\n90]
GeoHash算法
GeoHash 算法将二维的经纬度数据映射到一维的整数,所有元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近
将整个地球看成一个二维平面,然后划分成了一系列正方形的方格\n方格越小,坐标越精确
GeoHash 算法会继续对这个整数做一次 base32 编码 (0-9,a-z 去掉 a,i,l,o 四个字母) 变\n成一个字符串
进行 Geo 查询时,它的内部结构实际上只是一个zset(skiplist)
Redis 的 Geo 指令基本使用
Redis 提供的 Geo 指令只有 6 个
geoadd 指令携带集合名称以及多个经纬度名称三元组,geoadd company 116.562108 39.787602 jd
geodist 指令可以用来计算两个元素之间的距离,携带集合名称、 2 个名称和距离单位,geodist company code1 code2 km
scan
keys
Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key
keys没有 offset、limit 参数,一次性吐出所有满足条件的 key
keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令\n就会导致 Redis 服务卡顿
单线程程序,顺序执行所有指令,keys 指令执行完才继续
scan基础使用
scan 参数提供三个参数,第一个是cursor整数值,第二个是 key 的正则模式,第三\n个是遍历的 limit hint
scan 348 match keyliu* count 100
limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量
字典的结构
Redis 中所有的key都存储在一个很大的字典中,这个字典结构和Java中的HashMap一样,是一维数组 + 二维链表结构
第一维数组的大小总是 2^n(n>=0),扩容一次数组大小空间加倍,也就是 n++
scan 指令返回的游标就是第一维数组的位置索引,这个位置索引称为槽 (slot)
limit 参数就表示需要遍历的槽位数,有些槽位可能是空的
scan遍历顺序
scan的遍历顺序是采用高位进位加法来遍历
考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏
高位进位法从左边加,进位往右边移动,同普通加法正好相反,遍历所有槽位且没有重复
渐进式rehash
如果 HashMap 中元素特别多,线程就会出现卡顿现象。 Redis 为了解决这个问题,它采用渐进式 rehash
同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上
处于 rehash 中的字典,需要同时访问新旧两个数组结构
scan对与 rehash 中的字典,它需要同时扫描新旧槽位
更多的scan指令
zscan遍历zset集合元素,hscan遍历hash字典的元素、sscan 遍历set集合的元素
避免大key扫描
定位大 key redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1 指令每隔 100 条scan 指令就会休眠 0.1s
分支主题
string(字符串)
冗余空间减少内存频繁分配,动态扩容,最大长度为 512M
list(列表)
quicklist ,ziplist 不单纯双向链表 减少碎片化\n操作复杂度为 O(1),索引定位复杂度为O(n)
hash(字典)
HashMap 数组 + 链表二维结构,渐进式 rehash 策略\n存储消耗高于单个字符串,\n个数比较少且没有大的value时, 内部编码为ziplist
set(集合)
java中的HashSet,键值对无序唯一,内部相当于特殊字典
zset(有序列表)
Java 中的 SortedSet 和 HashMap 的结合体,内部实现用的是「跳跃列表」数据结构
容器型数据结构
list/set/hash/zset,容器不存在则创建再进行操作,容器没元素则删除释放内存
redis原理
线程IO模型
Redis单线程为什么还能这么快
基于内存计算,多路复用,基于select的事件轮询API,非阻塞IO
非阻塞IO
套接字,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程
在套接字对象上提供了一个选项 Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少
非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞
能读多少取决于内核为套接字分配读缓冲区内部的数据字节数
能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数
事件轮询(多路复用)
事件轮询主要是针对事件队列进行轮询,事件生产者将事件排队放入队列中,队列另外一端有一个线程称为事件消费者会不断查询队列中是否有事件
最简单的事件轮询 API 是 select 函数,它是操作系统提供给用户程序的 API
输入是读写描述符列表 read_fds &write_fds,输出是与之对应的可读可写事件
当有事件时,线程就挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环,我们把这个死循环称为事件循环,一个循环为一个周期
指令队列
Redis会将每个客户端套接字都关联一个指令队列
客户端的指令通过队列来排队进行顺序处理,先到先服务
响应队列
Redis为每个客户端套接字关联一个响应队列
Redis服务器通过响应队列来将指令的返回结果回复给客户端。
队列为空连接处于空闲状态,会将当前的客户端描述符从 write_fds 里面移出来
等到队列有数据了,再将描述符放进去。避免 select 系统调用立即返回写事件
定时任务
Redis定时任务记录在一个称为最小堆的数据结构中
堆中,最快要执行的任务排在堆的最上方。每个循环周期, Redis 会将最小堆里面已经到点的任务立即进行处理
处理完毕后,将最快要执行的任务还需要的时间记录下来,这个时间就是 select 系统调\n用的 timeout 参数。因为 Redis 知道未来 timeout 时间内,没有其它定时任务需要处理,所以\n可以安心睡眠 timeout 的时间
通信协议
RESP
RESP 是 Redis 序列化协议的简写。它是一种直观的文本协议,优势在于实现异常简\n单,解析性能好
Redis 协议将传输的结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符\n号\r\\n。1、 单行字符串 以 + 符号开头。2、 多行字符串 以 $ 符号开头,后跟字符串长度。3、 整数值 以 : 符号开头,后跟整数的字符串形式。\n4、 错误消息 以 - 符号开头。5、 数组 以 * 号开头,后跟数组的长度。
客户端到服务器
客户端向服务器发送的指令只有一种格式,多行字符串数组
一个简单的 set 指令\nset author liu 会被序列化成下面的字符串。\n*3\r\\n$3\r\\nset\r\\n$6\r\\nauthor\r\\n$8\r\\nliu\r\\n
服务器到客户端
服务器向客户端回复的响应要支持多种数据结构,所以消息响应在结构上要复杂不少
状态回复:在RESP中第一个字节为"+"
错误回复:在RESP中第一个字节为"-"
整数回复:在RESP中第一个字节为":"
字符串回复:在RESP中第一个字节为"$"
持久化
简介
Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制\n来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制
Redis 的持久化机制有两种,第一种是快照(RDB),第二种是 AOF 日志。快照是一次全量备\n份, AOF 日志是连续的增量备份。
快照是内存数据的二进制序列化形式,在存储上非常紧凑,\n而 AOF 日志记录的是内存数据修改的指令记录文本
定期进行 AOF 重写,给 AOF 日志进行瘦身
快照原理
RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 触发RDB久化过程分为手动触发和自动触发
Redis单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写
服务线上请求时, Redis 还要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,文件 IO 操作是不能使用多路复用 API
持久化的同时,内存数据结构不断改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完就没了
Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制很有意思
手动触发分别对应save和bgsave命令
使用save相关配置, 如“save m n”。 表示m秒内数据集存在n次修改\n时, 自动触发bgsave
如果从节点执行全量复制操作, 主节点自动执行bgsave生成RDB
默认情况下执行shutdown命令时, 如果没有开启AOF持久化功能则自动执行bgsave
fork(多进程)
Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,\n父进程继续处理客户端请求,子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段
在进程分离的一瞬间,内存的增长几乎没有明显变化
子进程做数据持久化,不会修改现有的内存数据结构,只是对数据结构进行遍历读取,然后序列化写到磁盘中
父进程则持续服务客户端请求,并对内存数据结构进行不间断的修改
redis使用操作系统的COW机制来进行数据段页面的分离。数据段由操作系统的页面组合而成,当父进程对其中页面的数据进行修改时,\n会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增\n长
每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面
子进程因数据没变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再不会改变,这是为什么Redis持久化叫「快照」原因
AOF原理
AOF日志存储的是Redis服务器顺序指令序列,AOF 日志只记录对内存进行修改的指令记录
AOF日志记录了Redis实例创建以来所有修改性指令序列,可以通过对一个空的Redis实例顺序执行所有的指令,也就是「重放」恢复Redis实例的内存数据结构
Redis在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令
Redis在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个\nAOF日志会非常耗时,导致长时间 Redis无法对外提供服务。需要对 AOF 日志瘦身
AOF重写
Redis提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身
开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到新的AOF日志文件中
序列化完毕后将操作期间发生的增量 AOF日志追加到这个新的AOF日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了
fsync
AOF日志以文件形式存在,当程序对AOF日志进行写操作时,是将内容写到内核为文件描述符分配的一个内存缓存中,内核异步将脏数据刷回到磁盘
如果机器突然宕机, AOF日志内容可能还没有来得及完全刷到磁盘时会出现日志丢失
glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘
Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失,由于是IO操作会慢
Redis是每隔 1s 左右执行一次 fsync 操作,周期 1s可以配置
混合持久化
重启 Redis 时,少使用 rdb 来恢复内存状态,因为丢失大量数据
使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,比较耗时
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文\n件的内容和增量的 AOF 日志文件存在一起
redis重启时,先加载rdb内容,再重放增量AOF日志就可以完全替代之前的AOF文件重放,效率得到提升
管道
(Pipeline) 并不是 Redis 服务器直接提供的技术,这个技术本质上是由客户端提供的,跟服务器没有什么直接的关系
客户端通过对管道中的指令列表改变读写顺序就可以大\n幅节省 IO 时间。管道中指令越多,效果越好。
连续的 write 操作根本没有耗时,之后第一个 read 操作会等待一个\n网络的来回开销,然后所有响应消息就都已经回送到内核的读缓冲了,后续的 read 操作\n直接就可以从缓冲拿到结果,瞬间就返回了
管道本质
客户端进程调用 write 将消息写到操作系统内核为套接字分配的发送缓冲 send\nbuffer
客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路\n由」送到服务器的网卡
服务器进程调用 read 从接收缓冲中取出消息进行处理
redis事务
Redis事务基本使用
事务操作都有begin、commit和rollback,begin事务的开始,commit事务的提交,rollback事务的回滚
Redis 事务操作分别是 multi/exec/discard。 multi 事务的开始,exec事务的执行,discard 事务的丢弃
所有指令在 exec 之前不执行,缓存在\n服务器的事务队列中,服务器收到 exec 指令,开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果
Redis单线程特性,不用担心在执行队列时被其它指令打搅
原子性
原子性是指要么事务全部成功,要么全部失败
Redis 的事务不能算「原子性」,仅仅是满足了事务的「隔\n离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利
Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键\n变量,否则会出错
discard(丢弃)
Redis为事务提供了discard 指令,用于丢弃事务缓存队列中所有指令,在exec执行之前
discard之后,队列中所有指令都没执行,就像multi和discard中间的所有指令从未发生过一样
优化
当事务内部的指令较多时,需要的网络 IO 时间也会线性增长。
Redis客户端执行事务时结合pipeline一起使用,可以将多次IO操作压缩为单次IO操作
redis的PubSub
PubSub
为了支持消息多播, Redis 不能再依赖于那 5 种基本数据类型了
单独使用一个模\n块来支持消息多播,这个模块名字叫 PubSub,也就是 PublisherSubscriber,发布者订阅者模型
subscribe命令发出后,需要休眠一会,通过get_message才拿到反馈消息
publish 命令发出后,需要休眠一会,通过 get\_message 才拿到发布消息
如果没有消息,get_message会返回空,告知当前没有消息,所以不是阻塞的
模式订阅
消费者订阅一个主题是必须明确指定主题的名\n称。如果我们想要订阅多个主题,那就 subscribe 多个名称
redis提供了模式订阅功能Pattern Subscribe,可以一次订阅多个主题,生产者新增加了同模式的主题,消费者也立即收到消息
消息结构
data 消息的内容,一个字符串
channel 表示当前订阅主题名称。
type消息类型,如果是普通消息,类型是message
pattern 它表示当前消息是使用哪种模式订阅到的,如果是通过 subscribe 指令订阅的,这个字段就是空。
PubSub缺点
一个消费者都没有,那么消息直接丢弃
一个消费者突然挂掉了,挂掉消费者重新连上时,这期间生产者发送消息,对这个消费者来说就是彻底丢失了
Redis停机重启,PubSub消息不会持久化,Redis宕机就相当于一个消费者都没有,消息直接被丢弃
小对象压缩
介绍
Redis 是非常耗费内存的数据库,它所有数据都放在内存里。如果不注意节约使用内存, Redis 就会因为无节制使用出现内存不足而崩溃
小对象压缩存储 (ziplist)
Redis 内部管理的集合数据结构很小,它会使用紧凑存储形式压缩存储
HashMap 是二维结构,如果内部元素比较少,使用二维结构反而浪\n费空间,使用一维数组进行存储,需要查找时,因为元素少进行遍历也很快,甚至可以比 HashMap 本身的查找还要快
ziplist是一个紧凑的字节数组结构,每个元素之间都是紧挨着,zlbytes/zltail 和 zlend 是 hash 结构,key和value会作为两个 entry 相邻存在一起\nRedis的intset 是一个紧凑的整数数组结构,存放元素都是整数的并且元素个数较少的set集合
hash-max-zipmap-entries 512 # hash 的元素个数超过 512 就必须用标准结构存储
内存回收机制
Redis不是将空闲内存立即归还给操作系统,为什么我们操作减少key没有立即有反应
系统回收内存是以页为单位,如果这个页上只要有一个 key\n还在使用,那么它就不能被回收
Redis 删除1GB 的key,这些 key 分散到很多页面中,每个页面都还有其它 key 存在,导致了内存不会立即被回收Redis 无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空\n闲内存
内存分配算法
内存分配是复杂的算法划分内存页,要考虑内存碎片,需要平衡性能和效率
Redis 使用 jemalloc(facebook) 库来管理内\n存,可以切换到 tcmalloc(googe)。因为 jemalloc 相比 tcmalloc 的性能要好些,所以\nRedis 默认使用了 jemalloc
第一部分类似tcmalloc,是分别以8字节、16字节、64字节等分隔开的
第二部分以分页为单位,等差间隔开
内存块管理通过一种chunk进行,一个chunk的大小是2^k (默认4 MB)。通过这种分配实现常数时间地分配small和large对象,对数时间地查询huge对象的meta(使用红黑树)
主从同步
作用
很多企业都没有使用到Redis集群,但是至少都做了主从
当master挂掉时,运维让从库过来接管,服务可以继续,否则master需要经过数据恢复和重启\n过程,会拖很长时间,影响线上业务服务
CAP原理
CAP原理就好比分布式领域牛顿定律,它是分布式存储的理论基石
C - Consistent ,一致性 A - Availability ,可用性,P - Partition tolerance ,分区容忍性
节点往往都是分布在不同机器上进行网络隔离开,意味着必然会有网络断开风险,这个网络断开的场景的专业词汇叫着「网络分区」
一句话概括 CAP 原理就是——网络分区发生时,一致性和可用性两难全
最终一致
Redis主从数据是异步同步的,所以分布式的 Redis 系统并不满足「 一致性」要求
当客户端在 Redis 主节点修改数据后,立即返回,主从网络断开的情况下,主节点依旧可以正常对外提供修改服务,所以 Redis 满足「 可用性」
Redis 保证「 最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点状态将保持一致
网络断开,主从节点数据出现大量不一致,网络恢\n复,从节点会采用多种策略努力追赶上落后数据,继续保持和主节点一致
主从同步
Redis同步支持主从同步和从从同步,从从同步是Redis后续版本增加的功能,为减轻主库同步负担
增量同步
Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本\n地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点
一边执行同步指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)
内存 buffer 是有限的,Redis 主库不能将所有的指令都记录在内存 buffer中
Redis 复制内存 buffer 是一个定长环形数组,如果组内容满了,就会从头开始覆\n盖前面的内容
如果网络状况不好,从节点无法和主节点进行同步,网络恢\n复时,Redis主节点中那些没有同步指令在buffer中有可能已经被后续指令覆盖掉\n了,从节点将无法直接通过指令流来进行同步,这个时候就需要用到更加复杂的同步机制 —\n— 快照同步
快照同步
先在主库上进行bgsave当前内存数据快照到磁盘中,再将快照文件内容全部传送到从节点
从节点将快照文件接受完毕,即执行一次全量加载,加载之前先要将当前内存数据清空
分支主题
配置合适的复制 buffer参数,避免快照复制的死循环
增加从节点
当从节点刚刚加入集群时,他必须先要进行一次快照同步,同步完成以后再继续进行增量同步
无盘复制
进行快照同步时,会进行IO操作,对于非 SSD 磁盘存储时,快照对系统负载有影响。
当系统正在进行AOF的fsync操作发生快照,fsync会被推迟执行,严重影响主节点的服务效率。
Redis2.8.18版开始支持无盘复制。所谓无盘复制是指主服务器直接通过套接字\n将快照内容发送到从节点,生成快照是一个遍历的过程
总结
主从复制是 Redis 分布式的基础, Redis 高可用离开了主从复制将无从进行
如果使用Redis持久化功能,认真对待主从复制,系统数据安全保障
wait
Redis复制是异步进行, wait 指令可以让异步复制变身同步复制,确保系统的强一\n致性 (不严格)。 wait 指令是 Redis3.0 版本以后才出现的
redis集群
Sentinel (哨兵模式)
简介
必须有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行从主切\n换,程序可以不用重启
自动切换,系统健壮,可用性高
Redis Sentinel集群一般是由 3~5 个节点组成,这样挂了个别节点集群还可以正常运转
持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为\n主节点
发生故障时,客户端会重新向 sentinel 要地\n址, sentinel 将最新主节点地址告诉客户端
消息丢失
Redis主从采用异步复制,当主节点挂掉,从节点没有收到全部同步消息,这部分未同步的消息就丢失了。
主从延迟特别大,丢失的数据就可能会特别多。 Sentinel无法保证完全不丢失,尽可能保证消息少丢失
两个选项可以限制主从延迟过大。min-slaves-to-write 1 min-slaves-max-lag 10
哨兵模式优点
哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
Sentinel 会不断的检查 主服务器 和 从服务器 是否正常运行。当被监控的某个 Redis 服务器出现问题,Sentinel 通过API脚本向管理员或者其他的应用程序发送通知。
哨兵模式缺点
Redis较难支持在线扩容,对于集群,容量达到上限时在线扩容会变得很复杂。
Sentinel基本使用
sentinel masters 展示所有被监控的主节点状态以及相关的统计信息
sentinel get-master-addr-by-name<master name> 返回指定<master name>主节点的IP地址和端口
主从切换后,之前的主库被降级到从库,所有的修改性的指令都会抛出 ReadonlyError
集群二 Codis
介绍
Codis分片原理
扩容
自动均衡
Codis代价
Codis优点
MGET指令的操作过程
Cluster 集群
定位算法
跳转
分支主题
迁移
redis扩展
新特性Stream
Info指令
分布式锁
redis过期策略
LRU
懒惰删除
保护Redis
Redis 安全通信
redis源码
「字符串」内部结构
ArrayList 内存中以字节数组形式存在
Redis 的字符串叫 SDS,有容量 长度 内容,24个bit 来记录 LRU 信息
Redis 的字符串有两种存储方式,emb 形式存储,raw 形式存储
扩容策略
小于1M采用加倍策略,超过1M每次\n扩容分配1M冗余空间
「字典」内部
dict是复合型数据结构\nhash,带过期时间的key结合zset都会用到
dict内部结构
dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的
字典数据结构的精华在 hashtable 结构上
通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链\n表数组中存储的是第二维链表的第一个元素的指针
渐进式rehash
Redis 使用渐进式 rehash 小步搬迁
搬迁操作埋伏在当前字典的后续指令中(来自客户端的 hset/hdel 指令等)
Redis 还会在定时任务中对字典进行主动搬迁
查找过程
redis会将 key 映射为一个整数,不同的 key 会被映射\n成分布比较均匀散乱的整数,保证hashtable平衡
hash函数
hashtable 的性能好不好完全取决于 hash 函数的质量
hash攻击
扩容条件
hash 表中元素的个数等于第一维数组的长度时,就会开始扩容
元素的个数达到第一维数组长度5倍强制扩容
缩容条件
缩容的条件是元素个数低于数组长度的 10%或hash表变得稀疏时
set的结构
Redis里set的结构底层实现也是字典,不过所有的value是NULL,其它的特性和字典一模一样。
压缩列表
压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任\n何冗余空隙
增加元素
ziplist 都是紧凑存储,没有冗余空间
ziplist占据内存大,重新分配内存和拷贝内存会有很大消耗
ziplist\n不适合存储大型字符串,存储的元素也不宜过多
级联更新
内容小于254 字节, prevlen 用 1 字节存储,否则就是 5 字节
entry 恰好都存储了 253 字节的内容,那么第一个 entry 内容的\n修改就会导致后续所有 entry 的级联更新,这是一个比较耗费计算资源的操作
IntSet小整数集合
intset 是紧凑的数组结构,同时支持 16 位、 32 位和 64 位整数
当 set 里面放进去了非整\n数值时,存储形式立即从 intset 转变成了 hash 结构
快速列表
介绍
quicklist 是 ziplist 和 linkedlist 的混合体,它\n将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串\n接起来
每个 ziplist 存多少元素
默认单个 ziplist 长度为 8k 字节
ziplist 的长度由配置参数 list-max-ziplist-size 决定
压缩深度
默认的压缩深度是 0,也就是不压缩
压缩的实际深度由配置参数 listcompress-depth 决定。为了支持快速的 push/pop 操作, quicklist 的首尾两个 ziplist 不压\n缩
跳跃列表
基本结构
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的
zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)
Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64 次方个元素
查找过程
从最高层一直到最底层的每一\n层最后一个比「我」小的元素节点列表,搜索路径
插入节点到底有多少层,有算法来分配一下,跳跃列表使用的是随机算法
随机层数
对新插入的节点,都需要调用一个随机算法给它分配一个合理层数
跳跃列表更加扁平化,层高相对较低,在单个层上需要遍历节点数量会稍多一\n点
记录一下当前的最高层数 maxLevel,遍历从这个 maxLevel 开始遍历性能提高很\n多
插入过程
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele)
找到搜索路径,创建新节点随机分配层数,通过前向后向指针串起来
删除过程
找到搜索路径,删除节点,重排前向后向指针,更新最高层数maxLevel
更新过程
Redis 遇到 score 值改变了就直接删除再插入,不会去判断位置是否\n需要调整
元素排名怎么算
Redis 在 skiplist 的 forward 指针上进行优化,给每个 forward 指针都增加了 span 属\n性
Redis 在操作时会小更新 span 值的大小
经过搜索路径节点的跨度 span 值叠加就可以算出rank值
紧凑列表
listpack
Redis 5.0 引入的数据结构 listpack对 ziplist 结构的改进,在存储空间\n上会更加节省,而且结构上也比 ziplist 要精简
listpack跟ziplist的结构几乎一摸一样,少了 zltail_offset 字段
skiplist 元素长度的编码为 1 个字节或者 5 个\n字节
listpack 元素长度的编码可以是 1、 2、 3、 4、 5 个字节
级联更新
listpack 的设计彻底消灭了 ziplist 存在的级联更新行为,元素与元素之间完全独立
取代 ziplist
ziplist 在 Redis 数据结构中使用太广泛了,替换起来复杂\n度会非常之高
使用在了新增加的 Stream 数据结构中
因为有很多兼容性问题还没有做好替换 ziplist 的准备
基数树
相关文件
redis/src/rax.h\nredis/src/rax.c
内存构造
Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix\nTree),按照 key 的字典序排列,支持快速地定位、插入和删除操作
rax 中有非常多的节点,根节点、叶节点和中间节点,有些中间节点带有 value,有些中\n间节点纯粹是结构性需要没有对应的 value
应用
rax 是 redis 自己实现的基数树, 它是一种基于存储空间优化的前缀树数据结构\n 在 redis 的许多地方都有使用到, 比如 streams 这个类型里面的 consumer group(消费者组) 的名称还有集群名称
缓存设计
缓存雪崩
大量数据同时过期
均匀设置过期时间,避免同一时间过期
互斥锁,保证同一时间内只有一个请求来构建缓存
双 key 策略,主 key设置过期时间,备 key永久,不会设置过期,主key过期,延伸备key
后台更新缓存,让缓存“永久有效”,异步更新,缓存预热
Redis 故障宕机
服务熔断
请求限流机制
openresty+Redis+Lua,漏斗限流,插件
构建 Redis 缓存高可靠集群
缓存击穿
热点数据过期
互斥锁,同一时间只有一个业务线程更新缓存
后台异步更新缓存
热点数据要过期时,由后台线程更新缓存以及重新设置过期时间
缓存穿透
访问数据不在缓存和数据库中
缓存空值或者默认值
布隆过滤器快速判断数据是否存在
黑客恶意攻击,大量访问不存在数据的业务
非法请求的限制
缓存污染
只被访问一次或者几次的数据,还在缓存中
消耗缓存空间,影响Redis性能
根据淘汰策略选择要淘汰的数据,进行删除操作
缓存收益成本
缓存更新策略
缓存粒度控制
无底洞的优化