深入浅出带你走进Redis!
本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯后台开发工程师刘波。
本文主要讲述Redis的基础知识和常识性内容,帮助大家了解和熟悉Redis;后续通过阅读源码、实践Redis后会总结相关的知识点,再继续分享给大家。
什么是Redis
Redis是一个开源、基于内存、使用C语言编写的key-value数据库,并提供了多种语言的API。它的数据结构十分丰富,基础数据类型包括:string(字符串)、list(列表,双向链表)、hash(散列,键值对集合)、set(集合,不重复)和sorted set(有序集合)。主要可以用于数据库、缓存、分布式锁、消息队列等...
以上的数据类型是Redis键值的数据类型,其实就是数据的保存形式,但是数据类型的底层实现是最重要的,底层的数据结构主要分为6种,分别是简单动态字符串、双向链表、压缩链表、哈希表、跳表和整数数组。各个数据类型和底层结构的对应关系如下:
各个底层实现的时间复杂度如下:
可以看出除了string类型的底层实现只有一种数据结构,其他四种均有两种底层实现,这四种类型为集合类型,其中一个键对应了一个集合的数据。
(一)Redis键值是如何保存的呢?
Redis为了快速访问键值对,采用了哈希表来保存所有的键值对,一个哈希表对应了多个哈希桶,所谓的哈希桶是指哈希表数组中的每一个元素,当然哈希表中保存的不是值本身,是指向值的指针,如下图。
其中哈希桶中的entry元素中保存了*key和*value指针,分别指向了实际的键和值。通过Redis可以在O(1)的时间内找到键值对,只需要计算key的哈希值就可以定位位置,但从下图可以看出,在4号位置出现了冲突,两个key映射到了同一个位置,这就产生了哈希冲突,会导致哈希表的操作变慢。虽然Redis通过链式冲突解决该问题,但如果数据持续增多,产生的哈希冲突也会越来越多,会加重Redis的查询时间。
Redis数据丢失问题
由上一小节我们大概了解了 Redis的存储和快的主要原因,通常情况下我们会把Redis当作缓存使用,将后端数据库中的数据存储在内存中,然后从内存中直接读取数据,响应速度会非常快。但是如果服务器宕机了,内存中的数据也就会丢失,当然我们可以重新从后端数据库中恢复这些缓存数据,但是频繁访问数据库,会给数据库带来一定的压力;另一方面数据是从慢速的数据库中读取的,性能肯定比不上Redis,也会导致这些数据的应用程序响应变慢。
所以对Redis来说,实现数据的持久化,避免从后端恢复数据是至关重要的,目前Redis持久化主要有两大机制,分别是AOF(Append Only File)日志和RDB快照。
(一)AOF日志
AOF日志是写后日志,也就是Redis先执行命令,然后将数据写入内存,最后才记录日志,如下图:
我们可以根据不同的场景来选择不同的方式:
- Always可靠性较高,数据基本不丢失,但是对性能的影响较大。
- Everysec性能适中,即使宕机也只会丢失1秒的数据。
- No性能好,但是如果宕机丢失的数据较多。
虽然有一定的写回策略,但毕竟AOF是通过文件的形式记录所有的写命令,但如果指令越来越多的时候,AOF文件就会越来越大,可能会超出文件大小的限制;另外,如果文件过大再次写入指令的话效率也会变低;如果发生宕机,需要把AOF所有的命令重新执行,以用于故障恢复,数据过大的话这个恢复过程越漫长,也会影响Redis的使用。
此时,AOF重写机制就来了:
AOF重写就是根据所有的键值对创建一个新的AOF文件,可以减少大量的文件空间,减少的原因是:AOF对于命令的添加是追加的方式,逐一记录命令,但有可能存在某个键值被反复更改,产生了一些冗余数据,这样在重写的时候就可以过滤掉这些指令,从而更新当前的最新状态。
AOF重写的过程是通过主线程fork后台的bgrewriteaof子进程来实现的,可以避免阻塞主进程导致性能下降,整个过程如下:
- AOF每次重写,fork过程会把主线程的内存拷贝一份bgrewriteaof子进程,里面包含了数据库的数据,拷贝的是父进程的页表,可以在不影响主进程的情况下逐一把拷贝的数据记入重写日志;
- 因为主线程没有阻塞,仍然可以处理新来的操作,如果这时候存在写操作,会先把操作先放入缓冲区,对于正在使用的日志,如果宕机了这个日志也是齐全的,可以用于恢复;对于正在更新的日志,也不会丢失新的操作,等到数据拷贝完成,就可以将缓冲区的数据写入到新的文件中,保证数据库的最新状态。
(二)RDB快照
上一小节里了解了避免Redis数据丢失的AOF方法,这个方法记录的是操作命令,而不是实际的数据,如果日志非常多的话,Redis恢复的就很缓慢,会影响到正常的使用。
这一小节主要是讲述的另一种Redis数据持久化的方式:内存快照。即记录内存中的数据在某一时刻的状态,并以文件的形式写到磁盘上,即使服务器宕机,快照文件也不会丢失,数据的可靠性也就得到了保证,这个文件称为RDB(Redis DataBase)文件。可以看出RDB记录的是某一时刻的数据,和AOF不同,所以在数据恢复的时候只需要将RDB文件读入到内存,就可以完成数据恢复。但为了RDB数据恢复的可靠性,在进行快照的时候是全量快照,会将内存中所有的数据都记录到磁盘中,这就有可能会阻塞主线程的执行。Redis提供了两个命令来生成RDB文件,分别是save和bgsave:
- save:在主线程中执行,会导致阻塞;
- bgsave:会创建一个子进程,该进程专门用于写入RDB文件,可以避免主线程的阻塞,也是默认的方式。
我们可以采用bgsave的命令来执行全量快照,提供了数据的可靠性保证,也避免了对Redis的性能影响。执行快照期间数据能不能修改呢?如果不能修改,快照过程中如果有新的写操作,数据就会不一致,这肯定是不符合预期的。Redis借用了操作系统的写时复制,在执行快照的期间,正常处理写操作。
主要流程为:
- bgsave子进程是由主线程fork出来的,可以共享主线程的所有内存数据。
- bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件中。
- 如果主线程对这些数据都是读操作,例如A,那么主线程和bgsave子进程互不影响。
- 如果主线程需要修改一块数据,如C,这块数据会被复制一份,生成数据的副本,然主线程在这个副本上进行修改;bgsave子进程可以把原来的数据C写入RDB文件。
写时复制机制保证快照期间数据可修改
通过上述方法就可以保证快照的完整性,也可以允许主线程处理写操作,可以避免对业务的影响。那多久进行一次快照呢?
理论上来说快照时间间隔越短越好,可以减少数据的丢失,毕竟fork的子进程不会阻塞主线程,但是频繁的将数据写入磁盘,会给磁盘带来很多压力,也可能会存在多个快照竞争磁盘带宽(当前快照没结束,下一个就开始了)。另一方面,虽然fork出的子进程不会阻塞,但fork这个创建过程是会阻塞主线程的,当主线程需要的内存越大,阻塞时间越长。
针对上面的问题,Redis采用了增量快照,在做一次全量快照后,后续的快照只对修改的数据进行记录,需要记住哪些数据被修改了,可以避免全量快照带来的开销。
(三)混合使用AOF日志和RDB快照
虽然跟AOF相比,RDB快照的恢复速度快,但快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?
在Redis4.0提出了混合使用AOF和RDB快照的方法,也就是两次RDB快照期间的所有命令操作由AOF日志文件进行记录。这样的好处是RDB快照不需要很频繁的执行,可以避免频繁fork对主线程的影响,而且AOF日志也只记录两次快照期间的操作,不用记录所有操作,也不会出现文件过大的情况,避免了重写开销。
通过上述方法既可以享受RDB快速恢复的好处,也可以享受AOF记录简单命令的优势。
对于AOF和RDB的选择问题:
- 数据不能丢失时,内存快照和AOF的混合使用是一个很好的选择。
- 如果允许分钟级别的数据丢失,可以只使用RDB。
- 如果只用AOF,优先使用everysec的配置选项,因为它在可靠性和性能之间取了一个平衡。
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中的修改操作发给从库,从库重新执行这些操作,就可以实现主从库同步了。
如果从库的实例过多,对于主库来说有一定的压力,主库会频繁fork子进程以生成RDB文件,fork这个操作会阻塞主线程处理正常请求,导致响应变慢,Redis采用了主-从-从的模式,可以手动选择一个从库,用来同步其他从库的数据,以减少主库生成RDB文件和传输RDB文件的压力;如下图:
例如:现在有3个哨兵,quorum配置的是2,那么,一个哨兵需要2张赞成票,就可以标记主库为“客观下线”了。这2张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。
这个时候哨兵就可以给其他哨兵发送消息,表示希望自己来执行主从切换,并让所有的哨兵进行投票,这个过程称为“Leader选举”,进行主从切换的哨兵称为Leader。任何一个想成为Leader的哨兵都需要满足两个条件:
- 拿到半数以上的哨兵赞成票。
- 拿到的票数需要大于等于quorum的值。
以上就可以选出Leader然后进行主从库切换了。
Redis集群
(一)数据量过多如何处理?
当数据量过多的情况下,一种简单的方式是升级Redis实例的资源配置,包括增加内存容量、磁盘容量、更好配置的CPU等,但这种情况下Redis使用RDB进行持久化的时候响应会变慢,Redis通过fork子进程来完成数据持久化,但fork在执行时会阻塞主线程,数据量越大,fork的阻塞时间就越长,从而导致Redis响应变慢。
Redis的切片集群可以解决这个问题,也就是启动多个Redis实例来组成一个集群,再按照一定的规则把数据划分为多份,每一份用一个实例来保存,这样客户端只需要访问对应的实例就可以获取数据。在这种情况下fork子进程一般不会给主线程带来较长时间的阻塞,如下图:
![](https://kz.cx/wp-content/uploads/2021/10/Pasted-11.png)