开篇
01| 基础架构
02| 底层数据结构
1、redis 底层的数据结构
2、redis 全局的hash结构
- 解决 Hash 冲突的方案 ==》 链地址法
扩容==》渐进rehash
2、压缩链表
- 压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
压缩链表中的每一个节点 entry1,都保存了前一个 entry 的长度,以此实现逆序遍历
3、跳表
- 增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,
- 创建的时候 取 1-32之间的随机数,以此作为当前节点的层数
4、String
- 修改了C 的字符串,底层是一个 动态字符串,支持扩容操作,同时保存了长度,不需要和C一样一个个的遍历 (等待补充)
5、list
等待补充
03| redis 线程模型
- 为什么 redis 那么快
- 大部分操作都在内存上完成,同时采用了高效的数据结构
- 多路复用机制
1、redis 的 io 模型
2、基于多路复用的高性能 I/O 模型
-
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个IO 流的效果
-
底层根据 linux 的版本,如果是用 epoll 调用的是 epoll_wait ,事件触发
-准确来说,Redis 的线程模式是 单 Reactor 模式。 NIO + epoll
3、性能瓶颈
- 对于 big key 的操作
- 潜在的大量数据操作, key * 等 引入了 scan 操作
- 单次操作耗时长就会影响后续所有的操作(合并、求差等)
04| 日志系统--AOF
1、AOF
- Redis 的 AOF 日志是 先写内存,再写日志的!!记住,和传统的 WAL 技术不同
- 记录的内容
“*3”表示当前命令有三个部分,每部分都是由“&+数字”开头,后面紧跟着具体的命令、键或值。这里,“数字”表示这部分中的命令、键或值一共有多少字节。例如,“$3 set”表示这部分有 3 个字节,也就是“set”命令
- 为什么选择后写日志:
- redis并没有存在检查操作,如果执行失败,可以直接不用写日志
- 命令执行结束后再写日志,不会阻塞当前的写操作
2、写回策略
- 注意 Always 必须要写命令 落盘后才能返回,中间是阻塞的
3、 AOF重写
Redis 记录的 AOF 日志 记录的是每一条操作,所以会不断增大,需要进行重写
-
“一个拷贝”就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
-
“两处日志”:写入 AOF重写日志 和 AOF 日志
-
注意点:AOF重写 和 AOF 没有关系, AOF重写是通过直接读取内存中的Redis 的数据,生成AOF重写日志文件,而后将AOF重写缓存中的数据加入到重写日志中,并将其与AOF日志文件进行替换实现的,过程中并不会阻塞主线程,采用的是 写时复制技术。
写时复制、写时重定向
4、阻塞点:
fork 子进程的瞬间会进行阻塞,需要负责一份父进程的内存的页表给子进程使用,在复制过程中的修改会采用写时复制技术进行实现
05|内存快照 -- RDB
-
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
-
save:在主线程中执行,会导致阻塞;
-
bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是Redis RDB 文件生成的默认配置。
-
1、快照时数据修改
采用写时复制技术进行修改
2、快照生成的周期
- 如果频繁执行全量快照,主要会造成两个方面的开下
- 磁盘压力,虽然是顺序写,但是IO 开销还是有的
- fork子进程的时候,需要复制主进程的页表,这时候会阻塞主线程,且主线程的内存占用越大,复制事件越长
3、混合使用AOF + RDB
- 内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
思考题
用一个 2 核 CPU、4GB 内存、500GB 磁盘的云主机运行 Redis,Redis 数据库的数据量大小差不多是 2GB,我们使用了 RDB 做持久化保证。当时 Redis 的运行负载以修改操作为主,写读比例差不多在 8:2 左右,也就是说,如果有100 个请求,80 个请求执行的是修改操作。你觉得,在这个场景下,用 RDB 做持久化有什么风险吗?你能帮着一起分析分析吗?
a、内存资源风险: Redis fork子进程做RDB持久化,由于写的比例为80%,那么在持久化过程中,“写实复制”会重新分配整个实例80%的内存副本,大约需要重新分配1.6GB内存空间,这样整个系统的内存使用接近饱和,如果此时父进程又有大量新key写入,很快机器内存就会被吃光,如果机器开启了Swap机制,那么Redis会有一部分数据被换到磁盘上,当Redis访问这部分在磁盘上的数据时,性能会急剧下降,已经达不到高性能的标准(可以理解为武功被废)。如果机器没有开启Swap,会直接触发OOM,父子进程会面临被系统kill掉的风险。
b、CPU资源风险: 虽然子进程在做RDB持久化,但生成RDB快照过程会消耗大量的CPU资源,虽然Redis处理处理请求是单线程的,但Redis Server还有其他线程在后台工作,例如AOF每秒刷盘、异步关闭文件描述符这些操作。由于机器只有2核CPU,这也就意味着父进程占用了超过一半的CPU资源,此时子进程做RDB持久化,可能会产生CPU竞争,导致的结果就是父进程处理请求延迟增大,子进程生成RDB快照的时间也会变长,整个Redis Server性能下降。
c、另外,可以再延伸一下,如果绑定了CPU,那么子进程会继承父进程的CPU亲和性属性,子进程必然会与父进程争夺同一个CPU资源,整个Redis Server的性能必然会受到影响!所以如果Redis需要开启定时RDB和AOF重写,进程一定不要绑定CPU。
06|数据同步-- 主从一致
Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式
1、同步过程
2、主从级联模式分担全量复制时的主库压力
- 通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
3、主从库间网络断了怎么办?
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,
- 如果在 repl buffer 中没有找到从库的同步位点,就进行全量复制
4、aof 和 rdb 的区别
- aof比rdb大,rdb加载起来比aof快
- 重启的时候 为什么用的是 AOF, 以为 AOF 文件的数据比较全
5、思考题
-
主从库间的数据复制同步使用的是RDB 文件,前面我们学习过,AOF 记录的操作命令更全,相比于 RDB 丢失的数据更少。那么,为什么主从库间的复制不使用 AOF 呢?
-
1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量同步的成本最低。
-
2、假设要使用AOF做全量同步,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量同步数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
07|哨兵模式
- 哨兵:就是一个运行在特殊模型的Redis进程,主要功能是: 监控--选主--通知
1、主观下线
- 哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”
2、客观下线
- “客观下线”的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。
3、选新主库 -- raft 算法
先通过 raft 选举出哨兵头
从库优先级、从库复制进度以及从库 ID 号。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
08|哨兵集群
1、 raft算法
(细节自己查查,还有一个ZAB)
- 任何一个想成为 Leader 的哨兵,要满足两个条件:
- 第一,拿到半数以上的赞成票;
- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
思考题
-
假设有一个 Redis 集群,是“一主四从”,同时配置了包含 5 个哨兵实例的集群,quorum 值设为 2。在运行过程中,如果有 3 个哨兵实例都发生故障了,此时,Redis 主库如果有故障,还能正确地判断主库“客观下线”吗?
-
因为判定主库“客观下线”的依据是,认为主库“主观下线”的哨兵个数要大于等于 quorum 值,现在还剩 2 个哨兵实例,个数正好等于 quorum 值, 所以还能正常判断主库是否处于“客观下线”状态。如果一个哨兵想要执行主从切换,就要获到半数以上的哨兵投票赞成,也就是至少需要 3 个哨兵投票赞成。但是,现在只有 2 个哨兵了,所以就无法进行主从切换了。
09|集群模式
- 单个redis 的内存过大,主要会导致 fork 子进程的时间过长
1、纵向扩展 和 横向扩展
- 纵向扩展的好处是,实施起来简单、直接。不过,这个方案也面临两个潜在的问题
-
第一个问题是,当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞
-
纵向扩展会受到硬件和成本的限制
-
- 横向拓展:数据切分
2、横向拓展的过程
- 在 Redis Cluster 方案中,一个切片集群共有 16384个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
- 在分配的时候,需要将 所有的 哈希槽 都分配完!!
1、根据 key 计算属于哪个槽
- 首先根据键值对的 key,按照CRC16 算法计算一个 16 bit的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
2、Move 选择
- 客户端给任意一个 redis 实例发送查询请求,如果想要访问的数据不在该 redis 实例上,将会返回 MOVE 信息,让客户端到其他 redis 实例上去查询
- 由于负载均衡,Slot 2 中的数据已经从实例 2 迁移到了实例 3,但是,客户端缓存仍然记录着“Slot 2 在实例 2”的信息,所以会给实例 2 发送命令。实例 2 给客户端返回一条 MOVED 命令,把Slot 2 的最新位置(也就是在实例 3 上),返回给客户端,客户端就会再次向实例 3 发送请求,同时还会更新本地缓存,把 Slot 2 与实例的对应关系更新过来。
3、 ASK选择
- Slot 2 正在从实例 2 往实例 3 迁移,key1 和 key2 已经迁移过去,key3 和key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令。ASK 命令表示两层含义:第一,表明 Slot 数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令
4、 ASK 转向 和 MOVE转向的区别
- move 转向会更新客户端的哈希槽分配信息, ask 不会更新
- ask 转向是一个中间状态,说明当前槽还没有迁移完成,但是你需要查找的数据在另一个 redis 实例中
- ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例
思考题
-
redis Cluster 方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的 key 做 CRC 计算,然后再和哈希槽做映射,这样做有什么好处吗?如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对 1 在实例 2 上,键值对 2 在实例 1 上),这样就不用计算 key 和哈希槽的对应关系了,只用查表就行了,Redis 为什么不这么做呢?
-
如果使用表记录键值对和实例的对应关系,一旦键值对和实例的对应关系发生了变化(例如实例有增减或者数据重新分布),就要修改表。如果是单线程操作表,那么所有操作都要串行执行,性能慢;如果是多线程操作表,就涉及到加锁开销。此外,如果数据量非常大,使用表记录键值对和实例的对应关系,需要的额外存储空间也会增加。
10|一些思考题和答案
1、问题:整数数组和压缩列表作为底层数据结构的优势是什么?
- 整数数组和压缩列表的设计,充分体现了 Redis“又快又省”特点中的“省”,也就是节省内存空间。整数数组和压缩列表都是在内存中分配一块地址连续的空间,然后把集合中的元素一个接一个地放在这块空间内,非常紧凑。因为元素是挨个连续放置的,我们不用再通过额外的指针把元素串接起来,这就避免了额外指针带来的空间开销
2、Redis 基本 IO 模型中还有哪些潜在的性能瓶颈?
- 在 Redis 基本 IO模型中,主要是主线程在执行操作,任何耗时的操作,例如 bigkey、全量返回等操作,都 是潜在的性能瓶颈。
3、AOF 重写过程中有没有其他潜在的阻塞风险?
- Redis 主线程 Fork 子线程的时候,需要将主线程的 PCB 拷贝给之子线程,在这个过程中会阻塞主线程,如果redis 内存占用很大,页表就会很大,fork 时间会很长
- 重现过程中采用的是 写时复制 技术,如果期间出现大量的修改操作,会占用大量的内存空间。
4、AOF 重写为什么不共享使用 AOF 本身的日志?
- 如果都用 AOF 日志的话,主线程要写,bgrewriteaof 子进程也要写,这两者会竞争文件系统的锁,这就会对 Redis 主线程的性能造成影响。
5、为什么主从库间的复制不使用 AOF?
- RDB 是二进制文件,经过了压缩,内容比较少,传输效率比较高
- 在从库进行恢复的时候,使用RDB进行恢复的速度比较快
6、主从切换的时候,客户端能否正常进行请求操作
- 从集群一般是采用读写分离模式,当主库故障后,客户端仍然可以把读请求发送给从库,让从库服务。但是,对于写请求操作,客户端就无法执行了。
7、如果想要应用程序不感知服务的中断,还需要哨兵或客户端再做些什么吗?
-
一方面,客户端需要能缓存应用发送的写请求。只要不是同步写操作(Redis 应用场景一般也没有同步写),写请求通常不会在应用程序的关键路径上,所以,客户端缓存写请求后,给应用程序返回一个确认就行。
-
另一方面,主从切换完成后,客户端要能和新主库重新建立连接,哨兵需要提供订阅频道,让客户端能够订阅到新主库的信息。同时,客户端也需要能主动和哨兵通信,询问新主库的信息。
8、为什么 Redis 不直接用一个表,把键值对和实例的对应关系记录下来?
- 如果使用表记录键值对和实例的对应关系,一旦键值对和实例的对应关系发生了变化(例如实例有增减或者数据重新分布),就要修改表。如果是单线程操作表,那么所有操作都要串行执行,性能慢;如果是多线程操作表,就涉及到加锁开销。此外,如果数据量非常大,使用表记录键值对和实例的对应关系,需要的额外存储空间也会增加
- 基于哈希槽计算时,虽然也要记录哈希槽和实例的对应关系,但是哈希槽的个数要比键值对的个数少很多,无论是修改哈希槽和实例的对应关系,还是使用额外空间存储哈希槽和实例的对应关系,都比直接记录键值对和实例的关系的开销小得多。
9、rehash 的触发时机 和 渐进执行机制
1.Redis 什么时候做 rehash?
-
Redis 会使用装载因子(load factor)来判断是否需要做 rehash。装载因子的计算方式是,哈希表中所有 entry 的个数除以哈希表的哈希桶个数。Redis 会根据装载因子的两种情况,来触发 rehash 操作:
- 装载因子≥1,同时,哈希表被允许进行 rehash;
- 装载因子≥5
-
如果装载因子小于 1,或者装载因子大于 1 但是小于 5,同时哈希表暂时不被允许进行 rehash(例如,实例正在生成 RDB 或者重写 AOF),此时,哈希表是不会进行 rehash 操作的。
10、采用渐进式 hash 时,如果实例暂时没有收到新请求,是不是就不做 rehash 了?
-
其实不是的。Redis 会执行定时任务,定时任务中就包含了 rehash 操作。所谓的定时任务,就是按照一定频率(例如每 100ms/ 次)执行的任务。
-
在 rehash 被触发后,即使没有收到新请求,Redis 也会定时执行一次 rehash 操作,而且,每次执行时长不会超过 1ms,以免对其他任务造成影响。
11、replication buffer 和 repl_backlog_buffer 的区别
-
replication buffer 是主从库在进行全量复制时,主库上用于和从库连接的客户端的 buffer,而 repl_backlog_buffer 是为了支持从库增量复制,主库上用于持续保存写操作的一块专用 buffer。
-
Redis 主从库在进行复制时,当主库要把全量复制期间的写操作命令发给从库时,主库会先创建一个客户端,用来连接从库,然后通过这个客户端,把写操作命令发给从库。在内存中,主库上的客户端就会对应一个 buffer,这个 buffer 就被称为 replication buffer。Redis 通过 client_buffer 配置项来控制这个 buffer 的大小。主库会给每个从库建立一个客户端,所以 replication buffer 不是共享的,而是每个从库都有一个对应的客户端。
-
repl_backlog_buffer 是一块专用 buffer,在 Redis 服务器启动后,开始一直接收写操作命令,这是所有从库共享的。主库和从库会各自记录自己的复制进度,所以,不同的从库在进行恢复时,会把自己的复制进度(slave_repl_offset)发给主库,主库就可以和它独立同步
11 | 底层数据结构
1、String
- String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存,
-
buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。
-
len:占 4 个字节,表示 buf 的已用长度。
-
alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len
-
对于 String 类型来说,除了 SDS 的额外开销,还有一个来自于 RedisObject 结构体的开销
-
int、embstr 和 raw 这三种编码模式,
- Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示:
1、压缩链表
Redis 有一种底层数据结构,叫压缩列表(ziplist),这是一种非常节省内存的结构
-
prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。
-
len:表示自身长度,4 字节;
-
encoding:表示编码方式,1 字节;
-
content:保存实际数据
12 | 有一亿个keys要统计,应该用哪种集合?
聚合统计
-
聚合统计,就是指统计多个集合元素的聚合结果,包括:统计多个集合的共有元素(交集统计);把两个集合相比,统计其中一个集合独有的元素(差集统计);统计多个集合的所有元素(并集统计)。
-
题目:统计手机 App 每天的新增用户数和第二天的留存用户数
- 以用一个集合记录所有登录过 App 的用户 ID
- 另一个集合 记录 每一天 登入过 APP 的用户
-
第一个集合:使用 set 作为记录, key表示为 user:id, value 是一个set 记录的是所有登入过的id
- 第二个集合: 同样使用set 作为记录, key表示为当天日期, value 是一个集合,记录的是当天登入过的 id
-
当日新增用户: 将 当日登入过的 ID 与 全部登入过的 ID 求差集合,找到在 单日登入,但是不在 过去登入的用户。
-
第二天留存用户: 将 当日登入过的 ID 与 昨天登入过的 ID 求并集,得到第二天留存用户
-
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。所以,我给你分享一个小建议:你可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。
2、排序统计
-
一般用在评论问题这种,需要展示出顺序的地方
-
最新评论列表包含了所有评论中的最新留言,这就要求集合类型能对元素保序,也就是说,集合中的元素可以按序排列,这种对元素保序的集合类型叫作有序集合。
-
区别:
- list ==》按照插入顺序进行排序
- set ==》 按照权重进行排序
-
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,建议你优先考虑使用 Sorted Set
3、二值状态统计
-
常用与 签到打卡 这种场景
-
每个用户一天的签到用 1 个 bit 位就能表示,一个月(假设是 31 天)的签到情况用 31 个 bit 位就可以,而一年的签到也只需要用 365 个 bit 位,根本不用太复杂的集合类型。这个时候,我们就可以选择 Bitmap。这是 Redis 提供的扩展数据类型。我来给你解释一下它的实现原理
案例1
假设我们要统计 ID 3000 的用户在 2020 年 8 月份的签到情况,就可以按照下面的步骤进行操作。
第一步,执行下面的命令,记录该用户 8 月 3 号已签到。
第二步,检查该用户 8 月 3 日是否签到
第三步,统计该用户在 8 月份的签到次数
案例2
- 如果记录了 1 亿个用户 10 天的签到情况,你有办法统计出这 10 天连续签到的用户总数吗?
- 分别 创建10个 一亿位 的 bitmap,分别去记录每一个用户登入和退出
最后将这 10个 一亿位的 Bitmap 进行 与 操作, 然后再进行统计 1 的个数
4、基数统计
-
基数统计就是指统计一个集合中不重复的元素个数。对应到我们刚才介绍的场景中,就是统计网页的 UV
-
网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。在 Redis 的集合类型中,Set 类型默认支持去重,所以看到有去重需求时,我们可能第一时间就会想到用 Set 类型。但是采用这种数据结构,会导致消耗大量的内存,你需要存储每一个用户的ID,如果该页面访问的人数较多。
-
HyperLogLog
-
HyperLogLog 是一种用于统计基数的数据集合类型,它的最大优势就在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小,但是存在一定的误差
13|GEO 数据类型
- GEO 常常用于地图上的定位问题,通俗来说,可以将高纬信息一维化,同时距离较近的所对应的一维信息较为相似
1、场景问题
- 场景问题: 用户需要查询附近的网约车信息?
GEOADD 命令:用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中;
GEORADIUS 命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内
-
假设车辆的ID 是33 ,经纬度信息是(116.034579,39.030452),我们可以采用GEO保存该信息
-
GEOADD cars:locations 116.034579 39.030452 33
-
当用户想要寻找自己附近的网约车时,LBS 应用就可以使用 GEORADIUS 命令。
-
LBS 应用执行下面的命令时,Redis 会根据输入的用户的经纬度信息(116.054579,39.030452 ),查找以这个经纬度为中心的 5 公里内的车辆信息,并返回给 LBS 应用。当然, 你可以修改“5”这个参数,来返回更大或更小范围内的车辆信息。
-
1 GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
14 | 在Redis中保存时间序列
- 时间序列常常用于物联网行业的数据记录,其特点如下
- 通常持续的,高并发写入
- 通常是插入数据,而不是更新数据
- 对于读取来说,既有对单条数据的查询,又有对范围数据的查询
1、基于Hash 和 Sort Set 保存时间序列
基于 Hash 进行保存
存在的问题:不能进行范围的查询
基于 sort set 进行保存
- 支持范围查询
15|基于redis 构建消息队列
- 需求
- 消息保存
- 重复消息处理
- 消息可靠性保证
1、基于 List 的消息队列
- List 天然的先进先出、非常适合消息队列的存储
- 潜在问题
- 不会主动去通知消费者有新的数据写入,消费者需要不断的轮询list
- 解决方案:采用 BRPOP 命令进行读取
- 阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据
- 重复消费问题
- 可以生成全局唯一的ID 号在消费端进行判断(重复消费问题是无法在消息队列中进行解决的)
- 可靠性问题
- 解决方案:通过Redis 自身的AOF 和 RDB 保证异常宕机情况
- 解决方案:实现一个手动ACK,在读取消息的同时将消息插入另一个list 中,
16 | 异步机制
1、redis 的阻塞点
客户端交互
网络IO
- Redis 采用的是基于时间驱动的IO多路复用,并不会阻塞
增删改查操作
- 按照时间复杂度进行区分
- 0(1) 增改查,不算阻塞
- O(n) 集合全体查询,聚合操作,第一个阻塞点
- BigKey删除操作:删除操作存在内存的释放过程,特别是针对 BigKey 的删除操作非常慢第二个阻塞点
- 同样删除,清空数据库也是阻塞的第三个阻塞点
磁盘交互
- Redis 较少用到磁盘,只有在AOF 和RDB适合使用
- AOF日志同步写第四个阻塞点
- 加载RDB 文件第五个阻塞点
切片集群交互
- 在切片集群的数据迁移,采用的都是rehash ,不存在阻塞
2、小结
- 常见的阻塞点:
- 集合全量操作和聚合操作(复杂操作)
- BigKey 删除操作
- 清空数据库
- AOF 同步写
- 从库加载RDB文件
3、那些操作可以修改成为异步的
- 读取操作--> 不能异步,需要等待读取结果
- 删除操作--> 可以实现异步操作
17 | 为什么CPU结构也会影响Redis的性能?
- 在主流的服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接
- 一个CPU 上面有多个物理核,物理核上面有多个逻辑核
1、CPU 的 NUMA 架构对 Redis 性能的影响
- 在实际应用 Redis 时,我经常看到一种做法,为了提升 Redis 的网络性能,把操作系统的网络中断处理程序和 CPU 核绑定。
- 在 CPU 的 NUMA 架构下,当网络中断处理程序、Redis 实例分别和 CPU 核绑定后,就会有一个潜在的风险:如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间。
- 为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis实例绑在同一个 CPU Socket 上
2、绑核的风险和解决方案
- 在给 Redis 实例绑核时,我们不要把一个实例和一个逻辑核绑定,而要和一个物理核绑定,也就是说,把一个物理核的 2 个逻辑核都用上。
- Redis 除了主线程以外,还有用于 RDB 生成和 AOF 重写的子进程
- 如果仅仅绑定了一个物理核,会导致其他子进程核主线程进行竞争
18 | 波动的响应延迟:如何应对变慢的Redis?(上)
1、慢查询命令
- 利用SCAN进行多次迭代返回
- 当执行排序、交集、并集等复杂操作的适合,可以在客户端进行操作
- Keys 可以转为 scan
2、过期删除策略
- Redis 键值对的 key 可以设置过期时间。默认情况下,Redis 每 100 毫秒会删除一些过期key,具体的算法如下
- 为什么需要给时间参数加上一个随机数,如果在同一个瞬间过期,会形成重复删除,直到过期的Key比例下降到25%以下,
3、文件系统:AOF模式
- always 策略并不是使用后台线程进行异步操作的
- AOF 的 always 会阻塞主线程
- AOF 重写会对磁盘大量的IO操作
操作系统的内存SWAP
- 物理内存不足
- 横向拓展
小结
-
获取 Redis 实例在当前环境下的基线性能。
-
是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
-
是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
-
是否存在 bigkey? 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
-
Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
-
Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现Redis 和其他内存需求大的应用共享机器的情况。
-
在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。
-
是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
-
是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket上。
思考题
-
哪些指令可以替代keys
-
1 keys命令 可以使用正则查找匹配的结果。 该命令有致命的缺点:没有limit,只能一次性获取所有符合条件的key。如果数据量很大的话,就会产生无穷无尽的输出。 keys命令是遍历算法,遍历全部的key,时间复杂度是O(N)。redis是单线程的,如果keys查询的时间过长,redis的其它操作会被阻塞较长时间,造成redis较长时间的卡顿。要避免在生产环境使用该命令。
-
2 scan命令 redis2.8版本之后,可以使用scan命令进行正则匹配查找。与keys不同的是,scan命令不是一次性查找出所有满足条件的key。而是根据游标和遍历的字典槽数,使得每次查询都限制在一定范围内。 cursor = 0时表示开始查询,第一次查询。每次查询结果中都会返回一个cursor,作为下次查询的开始游标。当某次查询返回的cursor=0时,表示已全部查询完毕。 相比于keys命令,scan命令有两个比较明显的优势:
-
scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。 scan命令提供了limit参数,可以控制每次返回结果的最大条数。
-
scan的缺点: 返回的数据有可能重复,需要我们在业务层按需要去重
20 | 删除数据后,为什么内存占用率还是很高?
-
当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。所以,操作系统仍然会记录着给 Redis 分配了大量内存
1、内存碎片
内因是操作系统的内存分配机制
内存分配器的分配策略就决定了操作系统无法做到“按需分配”。这是因为,内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配。
Redis 申请一个 20 字节的空间保存数据,jemalloc 就会分配 32 字节,此时,如果应用还要写入 10 字节的数据,Redis 就不用再向操作系统申请空间了,因为刚才分配的 32 字节已经够用了,这就避免了一次分配操作。
外因:键值对大小不一样和删改操作
这些键值对会被修改和删除,这会导致空间的扩容和释放。具体来说,一方面,如果修改后的键值对变大或变小了,就需要占用额外的空间或者释放不用的空间。另一方面,删除的键值对就不再需要内存空间了,此时,就会把空间释放出来,形成空闲空间。
2、 如何判断是否有内存碎片?
used_memory_rss
是操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片;而used_memory
是 Redis 为了保存数据实际申请使用的空间。
mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。
mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了
3、 如何清理内存碎片?
- 最简单的方式:重启redis
- 启动redis 的自动内存碎片清理功能
思考题
-
如果 mem_fragmentation_ratio 小于 1了,Redis 的内存使用是什么情况呢
-
分配的物理空间小于申请的空间,发生swap,严重降低读写性能
21 | 缓冲区:一个可能引发“惨案”的地方
1、客户端输入和输出缓冲区
2、输入缓冲区溢出
写入了 bigkey,比如一下子写入了多个百万级别的集合类型数据;
服务器端处理请求的速度过慢,例如,Redis 主线程出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在缓冲区越积越多。
通常情况下,Redis 服务器端不止服务一个客户端,当多个客户端连接占用的内存总量,超过了 Redis 的 maxmemory 配置项时(例如 4GB),就会触发 Redis 进行数据淘汰。一旦数据被淘汰出 Redis,再要访问这部分数据,就需要去后端数据库读取,这就降低了业务应用的访问性能。此外,更糟糕的是,如果使用多个客户端,导致 Redis 内存占用过大,也会导致内存溢出(out-of-memory)问题,进而会引起 Redis 崩溃,给业务应用造成严重影响
我们可以从两个角度去考虑如何避免,一是把缓冲区调大,二是从数据命令的发送和处理速度入手
输入缓存区的最大值是 1g,不能调整
Redis 为每个客户端设置的输出缓冲区也包括两部分:一部分,是一个大小为 16KB的固定缓冲空间,用来暂存 OK 响应和出错信息;另一部分,是一个可以动态增加的缓冲空间,用来暂存大小可变的响应结果。
3、输出缓冲区溢出
我为你总结了三种:
服务器端返回 bigkey 的大量结果;
执行了 MONITOR 命令;
缓冲区大小设置得不合理
4、 主从集群中的缓冲区
复制缓冲区的溢出问题
在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
所以,如果在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最终导致溢出
复制积压缓冲区的溢出问题
复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制
5、小结
- 从本质上看,缓冲区溢出,无非就是三个原因:命令数据发送过快过大;命令数据处理较慢;缓冲区空间过小。
-
针对命令数据发送过快过大的问题,对于普通客户端来说可以避免 bigkey,而对于复制缓冲区来说,就是避免过大的 RDB 文件。
-
针对命令数据处理较慢的问题,解决方案就是减少 Redis 主线程上的阻塞操作,例如使用异步的删除操作。
-
针对缓冲区空间过小的问题,解决方案就是使用 client-output-buffer-limit 配置项设置合理的输出缓冲区、复制缓冲区和复制积压缓冲区大小
-
思考题
我们提到 Redis 采用了 client-server 架构,服务器端会为每个客户端维护输入、输出缓冲区。那么,应用程序和 Redis 实例交互时,应用程序中使用的客户端需要使用缓冲区吗?如果使用的话,对 Redis 的性能和内存使用会有影响吗?
22 | 第11~21讲课后思考题答案及常见问题答疑
23 | 旁路缓存:Redis是如何工作的?
- 计算机系统中,默认有两种缓存:
- CPU 里面的末级缓存,即 LLC,用来缓存内存中的数据,避免每次从内存中存取数据;
- 内存中的高速页缓存,即 page cache,用来缓存磁盘中的数据,避免每次从磁盘中存取数
1、Redis 缓存处理请求的两种情况
24 | 替换策略:缓存满了怎么办?
1、LRU算法
-
LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据。
-
在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。具体来说,Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构RedisObject 中的 lru 字段记录)。然后,Redis 在决定淘汰的数据时,第一次会随机选出N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把lru 字段值最小的数据从缓存中淘汰出去。
redis 并不保证 lru 删除的一定是最长时间未访问的,只能说是在N 个数据中最小的
3、常见设置
优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,我建议你使用 allkeys-lru 策略。如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用allkeys-random 策略,随机选择淘汰的数据就行。
如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
25 | 缓存异常(上):如何解决缓存和数据库的数据不一致问题?
1、缓存中的数据和数据库中的不一致
- 出现删除失败的时候,可以采用重试机制进行,利用消息队列
2、小结
- 优先使用 删除数据库,然后更新缓存的方式
思考题
- 数据在删改操作时,如果不是删除缓存值,而是直接更新缓存的值,你觉得和删除缓存值相比,有什么好处和不足?
26 | 缓存异常(下):缓存雪崩、缓存击穿、缓存穿透
1、缓存雪崩
- 产生原因
- 缓存中的大量数据同时过期,解决方案:在数据过期的时候,设置一个随机数,增加个几分钟
- redis 发生了宕机,导致一下子很多的请求打到数据库
- 解决方案:服务降级、服务垄断,设置高可用集群,在数据过期的时候,设置一个随机
2、缓存击穿
- 产生原因
- 缓存中的一个非常热点的数据突然过期,导致所有的压力都到数据库
- 解决方案:对于非常热点的数据,不设置过期时间
3、缓存穿透
- 产生原因
- 查询大量不存在的数据
- 解决方案:布隆过滤器
-
布隆过滤器,一个非常大的bit数组,加上N 个hash函数,可以保证不存在的数据,一定不会查询到
-
27| 缓存被污染
缓存污染:缓存中存储了大量的只会访问一次的数据,导致缓存被污染
1、解决方案
random 策略===》效果非常有限
ttl 策略 ===》针对设置过期时间的数据,效果有限
LRU ===》数据访问的时效性,效果有限
LFU ===》增加数据计数器
2、LFU优化
8位 只有255 次数太小,改进算法:非线性递增
思考题
29| 无锁的原子操作:redis如何应对并发访问
- 并发访问如何解决: 加锁 原子操作
1、redis 实现原子操作的方法
LUA脚本
简单来说就是,LUA脚本做的事情尽量少一点
30| 如何使用redis 实现分布式锁
1 基于单个redis 节点实现分布式锁
-
NX 设置key
-
PX 设置过期时间
-
存在风险:
-
持有锁的对象宕机没有释放锁: 加上过期时间 PX
-
锁被其他人释放 : 通过设定不同客户端的唯一标识,自己加的锁只能自己释放
-
- 这几个操作必须是原子的
2 基于多个节点实现分布式锁
RedLock
- 加锁成功的两个条件:
- 从半数的 Redis 实例上获取到锁
- 获取锁的总耗时,小于锁的有效时
思考题
31 |ACID 属性
-
MULTI:开启一个事务
-
EXEC :提交事务
-
DISCARD:放弃事务
1、原子性
第一种:在执行EXE之前,操作命令本身存在错误,在进行EXE的时候,拒绝执行所有的操作,保证原子性
第二种:没有检查出错误,EXE继续执行,出现错误不会回滚,正确的会正常执行
第三种:执行EXEC命令。,但是Redis发生故障
小结
2、一致性
第一种:入队时候报错,不会执行==》保证了一致性
第二种: 执行的时候报错,错误的不会执行,正确的会执行===》保证了一致性
第三种: redis实例发生故障===》能不能恢复数据,数据库都是一致性的
3、隔离性
第一种:并发操作在EXE命令前执行,隔离性需要使用watch 机制进行保证
第二种: 并发操作在EXE命令之后执行,隔离性可以保证
4、持久性
取决于 reids 的配置
小结
思考题
32| 主从同步与故障切换
1、 主从数据不一致
-
主从复制是异步进行的
- 主要原因:
- (1)主从之间存在网络延迟,
- (2)从库可能执行比较慢
- 解决方案
2、读到过期数据
- 从库不会主动删除过期数据,吐过接收到的是 expire 的命令,那么从库上数据的过期时间将会延迟一定的时间,所以在业务上应该把过期时间设定为固定的具体的时间点,同时保存主从时间同步
3、不合理配置导致服务宕机
- 1、protecet-mode 决定的是哨兵能否被其他服务器访问,如果设置yes,无法实现主从切换
- 2、cluster-node-timeout,设置主从切换的判定时间,合理就可以
小结
思考题
33|脑裂:一次奇怪的数据丢失
-
脑裂问题:由于集群间的网络出现分区,或者网络延迟,同时出现了两个主节点
-
为什么 redis 有脑裂,zookeeper没有
- zookeeper 存在共识算法,需要半数以上的节点同意本次执行的结果才能写入成功,而redis 只需要主节点写入成功就可以
-
如何解决脑裂
-
只需要让之前的主库不能顺利执行任务就可以
36|redis支持秒杀
1、秒杀活动的特征
- 1、瞬时并发量大
- 2、读多写少
2、解决方案
- 活动前:用户会不断刷新产品的详情页
- 动静分离
- 秒杀活动开始
- 使用reids, 采用lua 脚本进行原子处理
- 成功的用户,可以进入到mysql进行进一步操作,或者先加入消息队列中
- 秒杀结束
- 并发量下降,没啥事
3、优化方案
加入一个库存是800,可以将其分散到 4台机器上 一个200,各自去维护
37|数据倾斜
- 数据倾斜:某一些节点上的数据量很大
- 访问倾斜:某一些节点上的热点数据很多,并发量较大
1、数据倾斜的主要原因
bigKey
- 某一个实例上,存在了一个bigkey
- 解决方案
Slot 分配不均衡
重新分配
Hash 算法导致
2、数据访问倾斜