Zset底层的数据结构是由压缩链表或跳表实现的
跳表在创建节点时,随机生成每个节点的层数。
跳表在创建节点时,先生成[0, 1]的随机数,如果随机数 < 0.25,层数就会增加一层;然后继续生成下一个随机数,直到随机数 > 0.25结束,最终确定该节点的层数
压缩列表是由连续内存块组成的顺序型数据结构,类似数组。
压缩列表的缺点:会发生连锁更新,导致压缩列表占用的内存空间要多次重新分配,这回直接影响到压缩列表的访问性能。
压缩列表只适合保存数据量不多的场景。
quicklist和listpack这两种数据结构就是为了尽可能地保持压缩列表节省内存的优势
在正常服务请求阶段,插入的数据都会写到哈希表1中,此时哈希表2没有被分配空间,随着数据量的增多,触发了rehash操作:
存在问题:如果哈希表1非常大,那么每次迁移到哈希表2的时候,可能会对redis造成阻塞,无法响应其他请求。
解决:在rehash进行期间,每次进行新增、删除、查找操作,redis除了操作哈希表1,还会操作redis2,这样就把一次大量数据的迁移工作分摊到了多个请求中。
注:哈希表扩容时,如果来了一个读请求,会现在哈希表1里查找;如果没找到,才会到哈希表2里查找。
redis中的string字符串使用SDS数据结构存储的,SDS结构如下:
SDS数据结构可以O(1)获取字符串长度,c语言的字符串需要O(n)
SDS不需要"\0"来表示字符串结尾,有个len来记录字符串的长度(但是她为了兼容部分c的库函数,还是加了"\0")
C语言的字符串在追加的时候是不安全的,程序内部不会判断缓冲区大小是否够用,当缓冲区发生溢出后会导致程序异常;SDS结构里加入alloc和len,这样可以通过alloc - len来判断剩余缓冲区的大小,当缓冲区大小不够用时,redis会自动扩大SDS的空间大小。
单线程的redis吞吐量可以达到10w/s
Redis的单线程指的是”接收客户端请求 -> 解析请求 -> 进行读写数据操作 -> 发送数据给客户端“这个过程是一个线程完成的,但是Redis程序不是单线程的,Redis在启动时,会启动后台线程
关闭文件、AOF刷盘、释放内存这些任务会创建单独的线程来处理,如果放在主线程来处理,Redis主线程就会发生阻塞。
Redis6也采用多个IO线程来处理网络请求,因为随着硬件的升级,Redis的性能瓶颈出现在网络IO的处理上
因为Redis是单线程的,所有的操作都是按顺序进行,由于读写操作等待用户的输入和输出都是阻塞的,IO多路复用就是为了单线程的服务同时处理多个客户端的请求。
多路:指多个网络连接客户端
复用:指复用同一个进程
IO多路复用是使用一个线程来检查多个Socket的就绪状态,在单个线程中通过记录跟踪每个Socket的状态来管理处理多个IO流
Redis的读写操作都是发生在内存的,但是redis重启后,内存中的数据就会丢失,为了保证内存中的数据不丢失,就需要持久化机制,把数据存储到磁盘,这样Redis重启后就能从磁盘中恢复数据,Redis主要有两种持久化方式,分别是:
AOF日志:每执行一条写操作,就会把该命令以追加的方式写入文件中,redis重启时,会逐一执行这个文件里的命令将数据恢复。redis由三种写回磁盘的策略:
RDB快照:RDB快照只记录一瞬间的内存数据,记录的是实际的数据。Redis有两个命令来生成RDB快照:
AOF日志记录的是操作命令,用AOF做故障恢复时,需要全量把日志全部执行一遍,一旦AOF日志非常多,会造成Redis恢复操作慢。所以引入了RDB快照(记录一瞬间的内存数据,记录的是实际数据,AOF文件记录的是命令操作的日志,而不是实际数据)
内存淘汰是在内存满时会触发内存淘汰策略来淘汰一些不必要的资源
过期删除是将已过期的键值对进行删除
内存淘汰:当Redis内存达到设置的阈值时,Redis就会主动挑选部分key删除以释放更多的内存。
Redis在每次处理客户端命令时,都会对内存使用情况判断,如果必要,则执行内存淘汰。内存淘汰的策略有:
redis的失效缓存不会立即删除,而是使用惰性删除 + 定期删除这两种策略配合
全量同步:以下情况会发生全量同步
步骤:
- 从服务器发送SYNC命令,开始请求同步
- 主服务器收到SYNC命令后,生成RDB快照,并将生成的RDB文件发送到从服务器中
- 从服务器收到RDB文件后,先清空当前的数据集,并载入RDB文件中的数据
- 在RDB文件传输的过程中,如果又有新的指令,主服务器会将新的指令先放在缓冲区中,一旦RDB文件传输完成,主服务器又会将缓冲区里的命令发给从服务器,保证数据的一致性。
增量同步:允许从服务器从断点处继续同步
步骤:
- 从服务器在网络恢复后,发送psync命令
- 主服务器收到psync命令后,告诉从服务器接下来要用增量同步的方式同步数据
- 主服务器将从服务器断开这段时间内执行的命令,发送给从服务器。
(断开这段时间内执行的命令会存在主服务器的环形缓冲区中,主要存放的是最近传播的写命令,如果缓冲区里的一部分数据还没同步给从服务器,就已经被覆盖了,主服务器就会再次采取全量同步)
redis的主从集群中,主从模式是读写分离的,如果主节点挂了,需要选择一个主节点变成从节点,如果没有哨兵机制,那么只能人工选择一个主节点变成从节点,还要通知其他从节点现在主节点的变更。
哨兵机制主要是实现了主从节点的故障转移,他会检测主节点是否存活,如果主节点挂了,就会选取一个从节点当成主节点,并且把新的主节点信息通知给其他的从节点。
故障节点主观下线:哨兵节点会定时对redis集群里的所有节点发送心跳包检测节点是否正常,如果一个节点没有恢复哨兵节点的心跳包,则认为该节点主观下线
故障节点客观下线:当一个节点被标记成故障,不代表这个节点下线了,但是如果哨兵集群中有超过quorum数量的哨兵节点认为该redis节点主观下线,则该redis客观下线
如果下线的redis节点是从节点或哨兵节点,则没有后续操作了;如果是主节点,则开始故障转移,从剩下的从节点中选出一个节点升级为主节点
哨兵集群中选择Leader:要从redis集群中选择一个节点变成主节点,必须先从哨兵节点中选取leader,一个哨兵节点至少要拿到quorunm个赞成票,才能成为Leader
哨兵Leader选择新主节点:哨兵Leader从redis从节点中选择一个redis节点作为主节点
Redis缓存数据量大到一台服务器无法缓存时,就需要使用Redis切片集群,将数据分布在不同的服务器上,降低对单主节点的依赖。
一个切片有16384个哈希插槽,这些哈希插槽类似数据分区,根据他的key被映射到一个哈希槽中。
本地缓存:将数据存储在本地应用程序或服务器上,用于加速数据访问,本地缓存通常是使用内存作为存储介质,利用内存的高速读写来提高访问速度。
本地缓存:由于本地缓存是存储在本地内存中,访问速度快,而且能够降低对远程服务器的访问次数;但是本地缓存的可扩展性收到硬件资源的限制,无法支持大规模的数据存储。
分布式缓存:将数据存储在多个分布式节点上,通过协同工作来提供高性能的数据访问服务,利用多台服务器来分担数据存储和访问的压力。
分布式缓存:节点可以动态扩展,能够支持大规模数据存储和访问的需求;但是相对本地缓存,分布式存储的访问速度相对慢,因为数据需要从多个节点进行访问,且需要通过网络进行数据传输。
分布式锁是分布式环境下并发控制的一种机制,用来控制某个资源在同一时刻只能被一个应用使用。
Redis本身可以被多个客户端共享,可以用来保存分布式锁,而且Redis的读写性能高,可以应对高并发锁的场景。
redis的set命令有个nx参数可以实现”key不存在时插入“,所以可以使用它实现分布式锁:
- 如果key不存在,则表示插入成功(加锁成功)
- 如果key存在,则表示插入失败(加锁失败)
加锁的操作要注意:
字符串类型的Key对应的Value值占用空间很大就是大Key问题
大Key问题会导致:内存占用过高,从而触发内存淘汰策略;大Key会占用大量内存,导致性能下降;会造成网络拥堵;会导致主从同步延迟
解决:
- 对大Key进行拆分,将一个含有数万成员的hashkey拆分成多个hashkey;
- 将不适用Redis的数据移到别的地方存储,并在Redis中异步删除这个数据
一般是以key的请求频率来判断的,例如:
解决:
- 将对应的热ky进行复制并迁移到其他的数据分片,来解决单个数据分片的热key压力
- 如果热key的产生来自读请求,可以使用读写分离架构来降低每个数据分片的读请求,也可以不断增加从节点。
缓存系统就是CAP中的AP,通过牺牲强一致性来提高性能,如果需要数据库和缓存保持强一致性,就不适合用缓存。
缓存的过期时间设置是太短、太长都不好:
- 太短的话,请求可能会比较多的落在数据库上,就失去了缓存的意义
- 太长的话,缓存中的脏数据会让系统长时间处于一个延迟的状态,会浪费内存。