扫二维码与商务沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
带你了解Redis
一、什么是 Redis
Redis 是一个开源、基于内存、使用 C 语言编写的 key-value 数据库,并提供了多种语言的 API。它的数据结构十分丰富,基础数据类型包括:string(字符串)、list(列表,双向链表)、hash(散列,键值对集合)、set(集合,不重复)和 sorted set(有序集合)。主要可以用于数据库、缓存、分布式锁、消息队列等...
以上的数据类型是 Redis 键值的数据类型,其实就是数据的保存形式,但是数据类型的底层实现是最重要的,底层的数据结构主要分为 6 种,分别是简单动态字符串、双向链表、压缩链表、哈希表、跳表和整数数组。各个数据类型和底层结构的对应关系如下:
1.1 Redis 键值是如何保存的呢?
Redis 为了快速访问键值对,采用了哈希表来保存所有的键值对,一个哈希表对应了多个哈希桶,所谓的哈希桶是指哈希表数组中的每一个元素,当然哈希表中保存的不是值本身,是指向值的指针,如下图。
其中哈希桶中的 entry 元素中保存了key 和value 指针,分别指向了实际的键和值。通过 Redis 可以在 O(1)的时间内找到键值对,只需要计算 key 的哈希值就可以定位位置,但从下图可以看出,在 4 号位置出现了冲突,两个 key 映射到了同一个位置,这就产生了哈希冲突,会导致哈希表的操作变慢。虽然 Redis 通过链式冲突解决该问题,但如果数据持续增多,产生的哈希冲突也会越来越多,会加重 Redis 的查询时间;
为了解决上述的哈希冲突问题,Redis 会对哈希表进行rehash操作,也就是增加目前的哈希桶数量,使得 key 更加分散,进而减少哈希冲突的问题,主要流程如下:
1.2 Redis 为什么采用单线程呢?
首先要明确的是 Redis 单线程指的是网络 IO和键值对读写是由一个线程来完成的,但 Redis 持久化、集群数据等是由额外的线程执行的。了解 Redis 使用单线程之前可以先了解一下多线程的开销。
通常情况下,使用多线程可以增加系统吞吐率或者可以增加系统扩展性,但多线程通常会存在同时访问某些共享资源,为了保证访问共享资源的正确性,就需要有额外的机制进行保证,这个机制首先会带来一定的开销。其实对于多线程并发访问的控制一直是一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果。即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。
这也是 Redis 使用单线程的主要原因。
1.3 Redis 单线程为什么还这么快?
IO 多路复用机制:使其在网络 IO 操作中能并发处理大量的客户端请求从而实现高吞吐率
IO 多路复用机制是指一个线程处理多个 IO 流,也就是常说的 select/epoll 机制。在 Redis 运行单线程的情况下,该机制允许内核中同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果,进而提升并发性。
Redis 是基于内存的,绝大部分请求都是内存操作,十分的迅速;
Redis 具有高效的底层数据结构,为优化内存,对每种类型基本都有两种底层实现方式;
主要执行过程是单线程,避免了不必要的上下文切换和资源竞争,不存在多线程导致的 CPU 切换和锁的问题;
二、Redis 数据丢失问题
AOF 为了避免额外的检查开销,并不会检查命令的正确性,如果先记录日志再执行命令,就有可能记录错误的命令,再通过 AOF 日志恢复数据的时候,就有可能出错,而且在执行完命令后记录日志也不会阻塞当前的写操作。但是 AOF 是存在一定的风险的,首先是如果刚执行一个命令,但是 AOF 文件中还没来得及保存就宕机了,那么这个命令和数据就会有丢失的风险,另外 AOF 虽然可以避免对当前命令的阻塞(因为是先写入再记录日志),但有可能会对下一次操作带来阻塞风险(可能存在写入磁盘较慢的情况)。这两种情况都在于 AOF 什么时候写入磁盘,对于这个问题 AOF 机制提供了三种选择(appendfsync 的三个可选值),分别是Always、Everysec、No具体如下:
三、Redis 数据同步
当 Redis 发生宕机的时候,可以通过 AOF 和 RDB 文件的方式恢复数据,从而保证数据的丢失从而提高稳定性。但如果 Redis 实例宕机了,在恢复期间就无法服务新来的数据请求;AOF 和 RDB 虽然可以保证数据尽量的少丢失,但无法保证服务尽量少中断,这就会影响业务的使用,不能保证 Redis 的高可靠性。
Redis 其实采用了主从库的模式,以保证数据副本的一致性,主从库采用读写分离的方式:从库和主库都可以接受读操作;对于写操作,首先要到主库执行,然后主库再将写操作同步到从库;
只有主库接收写操作可以避免客户端将数据修改到不同的 Redis 实例上,其他客户端进行读取时可能就会读取到旧的值;当然,如果非要所有的库都可以进行写操作,就要涉及到锁、实例间协商是否完成修改等一系列操作,会带来额外的开销;
主从库如何进行第一次数据同步
当存在多个 Redis 实例的时候,可以通过 replicaof 命令形成主库和从库的关系,在从库中输入:replicaof 主库 ip 6379 就可以在主库中复制数据,具体有三个阶段:
首先是主从库建立连接、协商同步的过程,具体的从库向主库发送 psync 命令,代表要进行数据同步;psync 中包含了主库的 runID(Redis 启动时生成的随机 ID,初始值为:?)和复制进度 offset(设为-1,代表第一次复制)两个参数,主库接收到 psync 命令会,会用 FULLRESYNC 命令返回给从库,包含两个参数:主库 runID 和复制进度 offset;其中 FULLRESYNC 代表的全量复制,会将主库所有的数据都复制给从库;
待从库接收到数据后,在本地完成数据加载,具体的主库执行 bgsave 命令,生成 RDB 文件,然后将文件发给从库,从库接收到 RDB 文件后,首先清空当前数据,然后再加载 RDB 文件;这个过程主库不会被阻塞,仍然可以接受请求,如果存在写操作,刚刚生成的 RDB 文件中是不包含这些新数据的,此时主库会在内存中用专门的 replication buffer 记录 RDB 文件生成后所有的写操作;
最后,主库会把 replication buffer 中的修改操作发给从库,从库重新执行这些操作,就可以实现主从库同步了。
四、Redis 集群
数据量过多如何处理?
当数据量过多的情况下,一种简单的方式是升级 Redis 实例的资源配置,包括增加内存容量、磁盘容量、更好配置的 CPU 等,但这种情况下 Redis 使用 RDB 进行持久化的时候响应会变慢,Redis 通过 fork 子进程来完成数据持久化,但 fork 在执行时会阻塞主线程,数据量越大,fork 的阻塞时间就越长,从而导致 Redis 响应变慢。
Redis 的切片集群可以解决这个问题,也就是启动多个 Redis 实例来组成一个集群,再按照一定的规则把数据划分为多份,每一份用一个实例来保存,这样客户端只需要访问对应的实例就可以获取数据。在这种情况下 fork 子进程一般不会给主线程带来较长时间的阻塞,如下图:
将 20GB 的数据分为 4 分,每份包含 5GB 数据,客户端只需要找到对应的实例就可以获取数据,从而减少主线程阻塞的时间。
当数据量过多的时候,可以通过升级 Redis 实例的资源配置或者通过切片集群的方式。前者实现起来简单粗暴,但这数据量增加的时候,需要的内存也在不断增加,主线程 fork 子进程就有可能会阻塞,而且该方案受到硬件和成本的限制。相比之下第二种方案是一种扩展性更好的方案,如果想保存更多的数据,仅需要增加 Redis 实例的个数,不用担心单个实例的硬件和成本限制。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。
陕西小伙伴科技
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流