Redis 作为一个网络存储服务,需要独立部署,业务侧通过网络访问,这样业务服务和数据存储可以解耦。Redis是一个单线程的网络IO模型,如何做到高性能呢? 后面会详细解释。
Redis支持多种命令,对外提供的协议也是自定义的RESP协议,客户端传过来的请求包需要经过解析识别出命令和参数,然后执行相应的逻辑。
内存键值数据库一般都会用哈希表作为索引,因为键值都是存储在内存中,内存的随机访问性能高,可以与哈希表的O(1)操作复杂度相匹配。
2. 存储模块
redis支持多种value数据结构,内存数据的维护是由内存分配器来维护的,对于GET/HGET等查询操作来说,根据value的存储位置返回value值即可。
对于SET/HSET 一个新值来说,内存分配器需要为其分配内存空间。
对于DELETE操作,内存分配器需要释放(或回收)内存空间。
因为Redis是基于内存的, 如果Redis节点重启了,数据就会丢失,Redis提供了持久化的能力, 包括内存快照(RDB)和持久化日志(AOF),以便Redis重启能根据快照和日志快速恢复数据。
3. 高可用支撑模块
如果只有一个Redis实例,这个实例挂掉了,就会影响整体服务,为了提高可用性,Redis通常部署多个实例,并且用主从架构,这样主库和从库之间有数据同步,主库挂了从库可以被选择成主库。如何感知主库挂了,怎么选举主库,这就引入了哨兵机制。
4. 集群可扩展
如果每个实例包含全量数据,单机内存是有限的,这样对于需要存储大规模的缓存数据是不满足要求的。为了能够横向可扩展,Redis引入数据分片(redis cluster)集群方案,将数据分片,每个节点存储部分数据,如果单个节点存储的内存过多,继续引入新节点进行分片,将全量数据分散到不同的节点上。
首先,说明一点,Redis是单线程的,主要是指Redis的网络IO和键值读写是由一个线程来处理的,也就是Redis对外提供服务的主要流程由一个线程来处理。但是,Redis的其他功能,比如持久化,异步删除,集群数据同步等,都是由额外的进程或线程来执行的。
这是因为如果用多线程,会有两个主要问题。一个是,多线程数据访问存在竞争,需要加锁,会带来额外的开销,特别是如果设计的不好,锁粒度控制的不好,系统吞吐率没有因为线程的增加而增加。 另外一个是,多线程会降低系统代码的易调试性和可维护性。
Redis单线程的模型,处理能力竟然能达到每秒十万级。一方面,Redis的所有请求处理都是基于内存操作的,并且Redis设计的数据结构都是很高效的。另一方面,Redis采用了IO多路复用机制,并发的处理大量的客户端请求, 并且大部分的请求都是比较轻量的操作,实现高吞吐率。Redis 网络框架调用epoll机制,让内核监听多个网络IO套接字,而epoll提供了事件回调机制,也就是针对不同的事件,调用相应的处理函数, 无需一直轮训套接字上是否有请求发生,可以避免造成CPU资源浪费。
Redis数据都是存储在内存中,为了down机之后能快速恢复内存数据,Redis提供了AOF日志和内存快照RDB机制。
与数据的写前日志(Write Ahead Log, WAL) 不同的是,Redis AOF(Append Only File)是写后日志。为了避免额外开销,Redis在向AOF文件记录日志的时候,并不会对这些命令进行语法检查,索引只有正确执行的命令请求才会被写入AOF文件。并且后写AOF可以避免阻塞当前请求。
AOF三种写会策略:
配置项 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,数据基本不会丢失 | 每个写命令都要落盘,会阻塞主线程 |
Everysec | 每秒写回 | 性能适中 | 宕机丢失1秒内数据 |
No | 操作系统控制写回 | 性能好 | 宕机时丢失数据较多 |
AOF重写机制:
AOF文件不能太大,首先文件系统对文件大小有限制,其次,文件太大,append效率也会低,最后,如果发生宕机,恢复内存数据过程会很缓慢。
AOF重写其实就是将Redis内存中当前所有键值创建一条命令记录它的写入。比如:
重写前: SET a 1,SET a 2,SET a 3
重写后: SET a 3
AOF重写会阻塞主线程吗:
AOF重写由后台子进程bgrewriteaof来完成的, 这也是为了避免阻塞主线程。
AOF重写的过程:
其实,AOF重写还是会有可能阻塞主线程的。虽然AOF重写时fork子进程是copy on write机制,但是如果Redis占用内存过大,fork一瞬间也会阻塞主线程,因为fork需要拷贝一些必要的数据结构,比如拷贝内存页表,拷贝过程会消耗大量CPU资源,这一瞬间会阻塞主线程。
Redis 提供了两个命令来生成RDB文件,分别是save和bgsave, 一个在主线程中执行,一个是创建子进程执行。默认是bgsave的方式,毕竟要不能阻塞主线程。bgsave也是采用copy on write机制,在执行快照的同时,正常处理读写。通用,bgsave也有AOF 重写的fork一瞬间可能会阻塞主线程的问题。
RDB虽然能恢复内存数据,但生成RDB是有成本的,因此不能执行过于频繁,但是这样又不能拿到最近的内存状态数据。在实际应用中,一般是结合RDB和AOF日志的方式来做内存数据恢复。也就是,RDB记录某个时间点的内存快照,再加上AOF这个时间点之后的操作来恢复内存数据。
如果Redis只有单个实例,这个实例宕机,服务就不可用。为了避免这种情况,通常是增加副本做冗余,将一份数据同时保存在多个实例上。即使一个实例出故障,其他实例也可以提供服务。
Redis提供主从模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式,主库可读可写,从库只能读。写操作经过主库同步给从库。
psync ? -1
其中有两个参数,第一个是runID, 每个Redis实例启动时都会生成的一个随机ID, 第一次同步,从库不知道主库的runID, 因此传入?。第二个参数offset表示复制进度,第一次传-1,表示从头开始复制全量。
2. 主库收到psync命令后,会用FULLRESYNC响应命令带上两个参数: 主库runID和复制进度offset,返回给从库。第一次复制,主库会给从库复制全量数据。
+FULLRESYNC {runID} {offset}
3. 主库执行bgsave命令,生成RDB文件, 并发送给从库。这个过程主库不会阻塞,仍然接收请求,为了保证主从数据的一致性,这期间收到的写请求会记录在专门的replication buffer。
4. 从库收到RDB文件和随后的replication buffer, 完成与主库的数据同步。
如果从库较多,启动的时候都从主库同步数据,会对主库压力很大。可以采用“主-从-从”模式来缓解,也就是从库也可以作为其他的实例的复制目标,这样将生产RDB和传输RDB的压力,以级联的方式分散到从库上。
主从网络断开连接后重新连上,如果每次都要全量复制,这个成本太高。主从库断连后,主库会把断连期间受到的写操作,写入replication buffer, 同时也写入repl_backlog_buffer (环形)这个缓冲区。从库重新连接后,根据repl_backlog_buffer 同步断开连接期间的操作。
------分割线--------
前面时候说主从架构解决单点问题,一个实例挂了,其他实例能够提供服务。其实,如果某一个从库挂了,整个redis集群可以提供读写服务,但是,如果主库挂了,redis集群就只能提供读服务了,因为写操作只有在主库上。这样就需要选举新的主库来提供写请求了。
哨兵其实是一个运行在特殊模式下的Redis进程,主要负责三个任务: 监控、选主和通知。
哨兵进程会用PING检测主库、从库的网络连接情况,用来判断实例状态。如果发现主库或者从库PING相应超时了,哨兵就会把它标记“主观下线”。
这里要避免误判的情况,比如主库没有下线,单纯由于哨兵某一次PING失败了,断定主库挂了,就启动主从切换,这样显然不合理。通常采用哨兵集群的模式来部署,相当于让多个哨兵同时做决策,这样误判率就能降低。只有大多数(N/2+1)哨兵实例都判断主库已经“主观下线”, 主库才会被标记为“客观下线”,才会进一步触发主从切换流程。
2. 选主: 筛选+打分
选主是由哨兵来完成,分为筛选和打分环节。
筛选:过滤非运行状态的,过滤容易出现网络故障的。
打分:从库优先级>从库复制进度>从库ID号。
3. 通知: 让从库执行replicaof,与新主库建立连接,通知客户端,与新主库连接。
哨兵是如何组成集群的呢?哨兵在启动的时候,只是跟主库建立连接,并不感知其他哨兵的存在,他是通过订阅主库的"__sentinel__:hello" 的频道,哨兵与主库建立连接时,就会把自己的IP和端口发布到这个频道上,所有哨兵都订阅这个频道,就能感知其他哨兵的存在了。
哨兵怎么知道从库信息的? 哨兵启动的时候只知道主库的IP和端口,与主库建立连接之后,通过给主库发送INFO, 获得从库信息, 并与从库建立连接。
哨兵怎么把通知发送给客户端? 哨兵上有多个消息频道,包括主库下线,从库重新配置事件,新主库切换等,客户端需要订阅这些消息频道,从而获取集群状态信息。
也是通过选举的方式,如果某个哨兵获得n/2+1的得票,就成为leader哨兵,具有主从切换的操作权限。
TODO
期待与您交流...