1、Redis的5种基础数据结构
Redis的5种基础数据结构:string (字符串)、list (列表 )、hash (字典)、 set (集合)、zset (有序集合)。
Redis所有的数据结构都以唯一的key字符串作为名称, 然后通过这个唯一key值来获取相应的value数据。
1.1 string (字符串)
Redis字符串是可以修改的字符串,在内存中是以字节数组的形式存在,字符串最大长度为512MB。
Redis字符串的结构叫“SDS”(Simple Dynamic String),是一个带长度信息的字节数组。
struct SDS<T> {
T capacity; // 数组容量,表示所分配的数组长度
T len; // 内容的实际长度
byte flags; // 特殊标志位,不用理睬它
byte[] content; // 数组的内容
}
1.2 list (列表)
Redis列表的内存结构:在列表元素较少的时候,所有的元素彼此紧挨着—起存储,分配的是一块连续的内存,这个结构是“压缩列表”(ziplist)。当列表元素较多的时候,会改成“快速链表”(quicklist)。因为普通的链表需要的附加指针空间太大,会浪费空间,还会加重内存的碎片化,比如某普通链表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将多个ziplist使用双向指针串起来使用,既满足了快速的插入删除性能, 又不会出现太大的空间冗余。
Redis列表插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。
1.3 hash(字典)
Redis字典类似于Java语言里面的 HashMap,它是无序字典,内部存储了很多键值对,结构为 “数组+链表” 。
Redis字典的值只能是字符串。
1.4 set (集合)
Redis集合相当于Java 语言里面的 HasbSet,它内部的键值对是无序的、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值NULL。
1.5 zset (有序列表)
Redis有序列表的内部实现用的是 “ 跳跃列表” ,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
1.6
list、set、hash、zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
- create if not exists:如果容器不存在,那就创建一个,再进行操作。比如rpush操作刚开始是没有列表的,Redis就会自动创建一个,然后再rpush进去新元素。
- drop if no elements:如果容器里的元素没有了,那么立即删除容器,释放内存。
Redis所有的数据结构都可以设置过期时间, 时间到了,Redis会自动删除相应的对象。
2、Redis原理
2.1 高性能
- Redis是基于内存的数据库,所有数据都在内存中,内存的读写速度远高于硬盘。
- Redis是单线程操作,避免了不必要的上下文切换和竞争锁机制,也不存在线程频繁切换导致CPU的消耗,所以Redis的操作速度非常快。Redis6.0引入了多线程,但执行请求命令的仍然是单线程。
- Redis采用了非阻塞I/O和多路复用(事件轮询)。非阻塞I/O意味着线程在读写I/O时可以不再阻塞,读写可以瞬间完成。多路复用是利用epoll(linux)、kqueue(FreeBSD和macosx)可以同时监听多个流的I/O事件的能力,在空闲的时候,会把当前网络线程阻塞掉,当某些网线程有I/O事件时,就从阻塞态中唤醒,按照轮询的顺序Redis依次进行处理,这样就可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),所以Redis具有很高的吞吐量。此处“多路”指的是多个网络请求,“复用”指的是复用同一个线程。
2.2 Redis的持久化机制
2.2.1 快照
- 快照持久化是由Redis的一个子进程来处理,子进程对数据(子进程产生时那一瞬间的数据)进行遍历读取,然后序列化写到磁盘中。
- 快照是一次全量备份,是内存数据的二进制序列化形式。
2.2.2 AOF日志
- AOF日志是Redis服务器的顺序指令序列,AOF日志只记录对内存进行修改的指令记录。
- Redis会在收到客户端修改指令后,进行参数校验、逻辑处理,如果没问题,就立即将该指令文本存储到AOF日志中,也就是说,先执行指令才将日志存盘。
- 如果机器突然宫机,AOF日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。
- AOF日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载AOF日志进行指令重放,这个时间就会无比漫长。
2.2.3 混合持久化
- 重启Redis时,如果用快照恢复内存状态,会丢失大量数据,如果用AOF日志重放,会非常慢。
- 混合持久化(Redis4.0之后提供)是将快照内容和增量的AOF日志文件存在一起,在Redis重启的时候,可以先加载快照的内容,然后再重放增量的AOF日志,重启效率得到大幅提升。
3、Redis集群
3.1 主从同步
- Redis的主从数据是异步同步,所以分布式的Redis系统不满足一致性要求。Redis尽力保证最终一致性,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态保持—致。如果网络断开了,主从节点的数据将会出现大量不—致,但一旦网络恢复,从节点会采用多种策略努力追赶,继续尽力保持和主节点一致。
- 当客户端在Redis的主节点修改了数据后,立即返回,即使在主从网络断开的清况下,主节点依旧可以正常对外提供修改服务,所以Redis满足可用性。
3.1.1 增量同步
Redis同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存buffer中,然后异步将buffer中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一边向主节点反馈自己同步到哪里了(偏移量)。因为内存的buffer是有限的,所以Redis主节点不能将所有的指令都记录在内存buffer中。Redis的复制内存buffer是—个定长的环形数组,如果数组内容满了,就会从头开始覆盖前面的内容。如果因为网络状况不好,从节点在短时间内无法和主节点进行同步,那么当网络状况恢复时,Redis的主节点中那些没有同步的指令在buffer中有可能已经被后续的指令覆盖掉了,从节点将无法直接通过指令流来进行同步。
3.1.2 快照同步
快照同步首先需要在主节点上将当前内存的数据全部快照到磁盘文件中,然后再将快照文件的内容全部传送到从节点。从节点将快照文件接受完毕后,立即执行一次全量加载。加载之前先要将当前内存的数据清空,加载完毕后通知主节点继续进行增量同步。在整个快照同步进行的过程中,主节点的复制buffer还在不停地往前移动,如果快照同步的时间过长或者复制buffer太小,都会导致同步期间的增量指令在复制buffer中被覆盖,这样就会导致快照同步完成后无法进行增最复制,然后会再次发起快照同步,如此极有可能会陷入快照同步的死循环。所以需要配置一个合适的复制buffer大小参数,避免快照复制的死循环。
3.2 Redis Sentinel(Redis哨兵)
Sentinel负责持续监控主从节点的健康,当主节点挂掉时,自动选择—个最优的从节点切换成为主节点。客户端来连接集群时, 会首先连接Sentinel,通过Sentinel来查询主节点的地址,然后再连接主节点进行数据交互。当主节点发生故障时, 客户端会重新向Sentinel要地址,Sentinel会将最新的主节点地址告诉客户端。如此应用程序将无须重启即可自动完成节点切换。
Redis主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。
如果主从延迟特别大,那么丢失的数据就可能会特别多。Sentinel无法保证消息完全不丢失,但是也能尽量保证消息少丢失。
下面两个选项可以限制主从延迟过大:
min-slaves-to-write 1
min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
第二个参数控制是否为正常复制,它的单位是秒(s),表示如果在10s内没有收到从节点的反馈,就意味着从节点同步不正常,要么是网络断开了,要么是一直没有给反馈。
3.3 Redis Cluster
Redis Cluster是Redis的“亲儿子”,它是Redis作者自己提供的Redis集群化方案,它去中心化的。
Redis Cluster将所有数据划分为16384个槽位,集群的每个节点负责其中一部分槽位,槽位的信息存储于每个节点中,当Redis Cluster的客户端来连接集群时,也会得到一份集群的槽位配置信息。这样当客户端要查找某个key时,可以直接定位到目标节点。
Redis Cluster可以为每个主节点设置若干个从节点, 当主节点发生故陓时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时,集群将完全处千不可用状态。
Redis Cluster是去中心化的,一个节点认为某个节点失联了并不代表所有的节点都认为它失联了,所以集群还得经过一次协商的过程,只有当大多数节点都认定某个节点失联了,集群才认为该节点需要进行主从切换来容错。
Redis集群节点采用Gossip协议来广播自己的状态以及改变对整个集群的认知。
比如一个节点发现某个节点失联了(PFail即Possibly Fail),它会将这条信息向整个集群广播,其他节点就可以收到这点的失联信息。如果收到了某个节点失联的节点数量(PFail Count)已经达到了集群的大多数,就可以标记该失联节点为确定下线状态(Fail),然后向整个集群广播,强迫其他节点也接受该节点已经下线的事实,并立即对该失联节点进行主从切换。