1. 什么是Redis?
Redis(Remote Dictionary Server),即远程字典服务,是一个开源的由C语言编写的基于内存的高性能key-value数据库。
2. Redis可以用来做什么?
Redis的读写速度非常快,每秒可以处理超过10万次读写操作,因此被广泛应用于缓存;
另外,Redis也可以用来做分布式锁。
3. Redis为什么性能高?
- Redis基于内存操作,也可以进行持久化到磁盘;
- Redis的操作是单线程的,避免了多线程中的频繁的上下文切换;
- Redis的数据类型的底层是专门设计的一些高效的数据结构;
- Redis使用了I/O多路复用模型。
4. Redis为什么使用单线程操作?
因为Redis是基于内存的操作,CPU不是性能的瓶颈。
值得注意的是,Redis的单线程是指其读写操作是单线程的,而一个Redis服务器不可能完全是单线程的,比如在进行持久化的时候就使用了子进程的方式来完成。
5. Redis有哪些基本数据类型?
Redis是一种key-value内存数据库,key都是字符串类型,而value都是自定义的RedisObject对象。
RedisObject对象有3个重要的属性:
- type: 类型,表示value的数据类型,主要有5种;
- encoding:编码,表示底层的数据结构;
- ptr:指针,指向保存value的底层数据结构的指针。
(1) String
字符串,底层由Redis自定义的SDS实现(simple dynamic string)。
为什么要自定义SDS而不是使用C语言中的字符数组呢?
- 提供了len属性保存字符串长度,而不需要遍历字符数组进行计算;
- SDS支持动态拓展空间,防止C语言字符数组申请的缓冲区溢出;
- 保证二进制安全,C语言字符数组中不能出现 \0.
(2) List
列表,在Redis 3.2版本之前,底层由压缩列表(ZIPLIST)以及双向链表(LINKEDLIST)实现,在3.2版本之后,使用快速列表(QUICKLIST)实现。
1) 压缩列表(ZIPLIST)
压缩列表的目的是为了节约内存空间,它按照类似数组的方式进行存储,但是又有所不同。
数组中每个元素占用的空间都相同,会造成很大的空间浪费,而压缩列表对元素进行了压缩,使得每个元素的大小为实际存储的大小,并用一个length属性来表示当前元素的大小。
压缩列表的缺点是不能存储过多的元素,否则遍历效率会很低;而且如果新增或修改元素时,可能需要重新分配内存,导致连锁更新,影响性能。
2) 双向链表(LINKEDLIST)
双向链表的每个节点都维护了上一个节点和下一个节点,在增删时非常方便,时间复杂度为O(1)。
双向列表的缺点是查询时需要进行遍历,时间复杂度为O(n);另外,由于节点内存不连续,不能很好的利用CPU缓存。
3) 快速列表(QUICKLIST)
快速列表结合了压缩列表和双向链表的优点,它是一个以压缩列表为节点的双向链表。
(3) Hash
映射,相当于Java中的Map或者对象。可以直接修改Hash对象中某个属性,而不用修改整个Hash对象。 它的底层编码有两种:ZIPLIST和HASHTABLE。
1) 压缩列表(ZIPLIST)
与前文相同,不赘述。
2) 哈希表(HASHTABLE)
HASHTABLE可以保存key-value键值对,也是通过拉链法来解决哈希冲突。但是随着存储数据量的增大,哈希冲突的可能性也会越来越高,导致链表越来越长,查询某个key时可能需要遍历整个链表,大大降低了查询效率。
因此,在数据量达到一定量时,会进行扩容,也就是rehash。
触发rehash的条件如下,二者满足一个即可:
- 负载因子 >= 1,并且Redis服务器没有执行BGSAVE(生成RDB)或BGREWRITEAOF(重写AOF)命令时,会执行rehash操作;
- 负载因子 >= 5,强制进行rehash操作。
但是Redis是高性能的,如果进行rehash,就会很影响性能,那该怎么办?
Redis使用了渐进式rehash。
渐进式rehash的意思是,不是一次性的把所有数据rehash到新的哈希表中,而是以数组下标为单位多次进行。
每次对哈希表元素进行增删改查时,会将原哈希表中某个下标处(rehashindex = 0,表示开始rehash)的所有数据rehash到新的哈希表中,然后下次再进行增删改查时,再把下一下标处(rehashindex = 1)的所有元素rehash到新的哈希表中,直到原哈希表中的所有数据全部rehash完成(rehashindex重新设置为-1,表示rehash结束)后,整个渐进式rehash动作结束。这样就巧妙地解决了一次性hash带来的耗时问题。
在底层其实有两个哈希表ht[0]和ht[1],两个哈希表中都会存储一部分的数据,所以,在对哈希表进行查询、修改、删除操作时,会先从ht[0]中查找,找不到就会去ht[1]中查找。但向哈希表中新增数据时,只能添加到ht[1]中,这样才能确保ht[0]中的数据越来越少,可以顺利完成rehash操作。
(4) Set
集合,底层由INTSET或者HASHTABLE实现。
1) INTSET
当一个集合中只包含整数值元素时,会使用此种编码方式。
2) HASHTABLE
与前文相同,不赘述。
(5) ZSet
有序集合,ZSet中的每个元素都有一个score属性,用来进行排序,查询时会按照由小到大的顺序排列。
ZSET的底层编码有两种:ZIPLIST和SKIPLIST。
1) 压缩列表(ZIPLIST)
与前文相同,不赘述。
2) 跳表(SKIPLIST)
跳表是一个基于有序列表实现的可以快速查找元素的数据结构。
跳表在原有链表结构的基础上,每跳过一个元素,使得两个元素相连,在上层组成一条新的链表,以空间换时间。基于这种思想,还可以继续在上层添加新的链表。
比如若要寻找元素6,在原链表中(第0层)需要遍历6个节点,而通过跳表,只需要遍历3个节点(1 -> 5 -> 6)即可。
6. Redis缓存问题有哪些?
(1) 缓存穿透
描述:是指大量请求访问数据库中不存在的数据,因此也无法写入缓存,从而造成数据库压力过大。
解决:① 写入有过期时间的NULL值; ② 前置布隆过滤器判断值是否存在,可能会误判。
(2) 缓存击穿
描述:是指热点key到期,从而导致数据库流量激增。
解决:① 设置热点key不过期; ② 通过分布式锁,只需允许一个线程访问数据库并设置缓存。
(3) 缓存雪崩
描述:是指大量key到期,从而导致数据库流量激增。 解决:① 设置热点key不过期; ② 随机设置过期时间,避免同时到期。
6. Redis分布式锁的原理是什么?
Redis分布式锁的本质是通过lua脚本对变量值进行设置,如果能设置成功,则说明获取到了锁。 在加锁时,需要设置过期时间和UUID。
为什么要使用lua脚本?
lua脚本可以保证操作的原子性
为什么要设置过期时间?
防止锁死。如果加锁的线程崩溃,或是由于其他问题导致没能释放锁,则锁永远都不能释放。
为什么要设置UUID? 防止误删除。如果加锁的线程在过期时间内没有完成业务,那么锁也会释放,由别的线程获取到锁。此时之前的线程再进行解锁操作,则会误删除别的线程假的锁。UUID不会重复,只有线程发现要删除的锁的UUID是自己设置的值,才会删除锁。
7. Redis持久化的方式?
Redis提供两种持久化的方式:RDB和AOF
(1) RDB文件
RDB文件是Redis每隔一定的时间间隔,对当前数据进行的一次快照。
优点: 体积更小:RDB文件中的数据是以二进制的压缩文件进行存储; 恢复更快:RDB是数据的快照; 性能更高:可以通过fork一个子进程的方式进行RDB文件的生成。
缺点: 数据丢失:在时间间隔内,若Redis宕机,则该部分数据会丢失。
(2) AOF文件
AOF文件是将每次写操作记录到文件的末尾。
优点: 数据完整:默认以每秒一次的速度写入aof文件,丢失数据少。(实际上,写入的只是aof缓冲区,然后由操作系统的刷盘策略决定何时写入磁盘)
缺点: 体积更大:不断追加指令,会使得文件过大。(Redis提供了重写机制,当AOF文件的大小达到某个阈值的时候,就会把这个文件中的重复命令或可以合并的命令进行重写) 恢复较慢:由于存储的是指令,所以相比RDB恢复数据更慢些。
(3) 混合持久化
为了结合上述两种方式的优点,Redis 4.0新增了混合持久化的方式。
在进行重写之后,AOF文件的前半部分是RDB格式的数据,后半部分是AOF格式的命令。
即,AOF文件包括RDB文件的内容,以及从RDB持久化开始到结束期间发生的增量AOF日志。
8. Redis有哪些架构模式?
(1) 单机模式
仅一个节点,部署简单,成本低,但是单节点有单点故障问题,一般不采用。
(2) 主从模式
主从模式由一个Master节点和多个Slave节点组成。
Master节点负责读写操作,而Slave节点只负责读操作,它的数据是从Master复制过来的。
主从复制可以分为两种:全量复制和增量复制。
全量复制一般在Slave启动时执行:
Slave向Master发送psync命令,Master接收到命令后,会fork出一个子进程,用以生成RDB文件。
在生成RDB文件的时候,新的写命令会写入缓冲区中。
RDB文件生成后,会发送给Slave,Slave清除掉自己的数据,并加载RDB文件。
RDB发送完成后,再将缓冲区中命令发送给Slave,主从数据就一致了。
增量复制一般在网络出现中断,恢复后执行:
Redis有一个环形缓冲区,用于记录Master接收的写请求以及对应的偏移量,随着offset的不断增大,会覆盖掉之前的数据。
Slave向Master发送psync命令时,其实还会携带一个offset,表示当前数据复制的偏移位置。如果offset已经不在环形缓冲区中了,就进行全量复制。如果offset还在环形缓冲区中,则Master将缓冲区中的写命令发送给Slave,完成增量复制。
详细的流程可以参考这篇文章。
主从模式的缺点是,如果Master宕机,就必须进行手动选主。
(3) Sentinel哨兵模式
Sentinel哨兵模式是通过Sentinel集群来监控节点状态,在Master宕机时进行自动选主,实现故障恢复。
Sentinel模式的原理是:
每个Sentinel都是一个独立的进程,它以每秒一次的频率,向所有的Redis节点和Sentinel节点发送PING命令。
如果某个节点超过一定的时间未响应,则会被标记为主观下线。
如果有足够数量的Sentinel都认为该节点主观下线,则该节点会被标记为客观下线。
如果Master被标记为客观下线,则Sentinel集群会先协商投票选出一个Sentinel Leader,然后由该Sentinel Leader从Slaves中选择一个作为新的Master,其余的Slaves重新从新Master进行主从复制。
Sentinel模式的缺点是,单个节点的存储容量是有限的,毕竟只有Master在真正写入数据,其余节点都是从Master进行复制。
(4) Cluster集群模式
Cluster模式是使用分片来存储数据,可以认为是多个主从模式的集合,每个Master保存一部分数据。
Cluster模式将键空间分为2^14个slots,分布在不同的Master上。
数据在写入时,将缓存key通过CRC16算法,计算出所属的slot,并存储在对应的Master上,其Slaves从Master进行主从复制。
如果某个Master故障,其Slaves会发送请求给其他Master进行投票,得票超过半数的Slave被选举为新Master,其他Slaves从新Master进行数据复制。
如果需要继续水平扩容,可以通过Redis的内部管理软件redis-trib执行重新分片,数据也需要重新迁移,但Redis服务并不需要下线。
9. 什么是脑裂问题?该如何处理?
上边我们说了,如果Master宕机会从Slaves中选择一个新的Master,但是如果老Master只是由于网络或者别的问题导致的临时的“假宕机”,待它恢复后,此时就会有两个Master了,这就是脑裂问题。
脑裂问题最严重的后果就是数据丢失:因为新Master会让所有的Slaves删除自身数据,重新进行全量复制,如果写请求还是向老Master写入,那么数据就丢失了,因为老Master最终也会成为Slave。
那该如何解决脑裂问题呢?
解决脑裂问题的根本方法是通过限制老Master的写入请求。为此Redis提供了两个配置项:
- min-slaves-to-write:与Master通信的Slaves数量必须大于等于该值,否则主节点拒绝写入;
- min-slaves-to-lag:主从之间通信的ACK消息延迟必须小于该值,否则主节点拒绝写入。
脑裂问题能彻底解决吗?
通过上边两个参数的合理配置只能尽量规避脑裂问题,并不能彻底解决。
如果min-slaves-to-lag比判断Master客观下线时间长,那么选择新Master时,老Master还在写入;
如果min-slaves-to-lag比判断Master客观下线时间短,那么就会出现无法写入的情况。
Redis脑裂问题的本质是为了可用性,牺牲了强一致性。
10. Redis的过期删除策略有哪些?
key到期之后需要进行删除,那么Redis是如何判断哪些key过期了呢?
-
定时删除 最简单的想法就是针对每个key进行计时,到期后执行删除。
但是这种方式很消耗CPU,所以Redis没有选用。 -
惰性删除
当下次使用某key时,判断其是否过期,如果过期就重新从数据库读取并写入缓存。
这种方式有个缺点,如果某些key早已过期,但是由于一直没有被使用所以没有能够删除,则会浪费内存。 -
定期删除
Redis默认每隔100ms就随机选择一些设置了过期时间的key,看看有没有过期,如果过期就删除。
这种方式也有缺点,如果某些key早已过期,但是一直没有被选择到,那么就一直不会被删除。
综合考虑之后,Redis选择的过期策略是:定期删除 + 惰性删除。
11. Redis的内存淘汰策略有哪些?
实际上,就算是使用了定期删除 + 惰性删除的过期策略,还是会漏掉一些过期的key。而且随着写入的key的增多,Redis的内存总有不够的时候,这时候就需要内存淘汰策略了,主要有以下8种策略。
- 不淘汰;(内存空间达到设置的maxmemory之后,写请求会失败并返回错误)
- 随机移除key;
- 移除使用最近未使用的key;(LRU)
- 移除使用最少的key;(LFU)
- 在设置了过期时间的key中随机移除;
- 在设置了过期时间的key中移除最近未使用的;(LRU)
- 在设置了过期时间的key中移除使用最少的;(LFU)
- 在设置了过期时间的key中移除快要过期的。
12. Redis使用时有哪些优化策略(如何调优)?
(1) 从服务端考虑
1) 限制Redis内存大小
默认Redis的内存大小是没有限制的,即maxmemory没有设置,这样的话在内存不足的时候,Redis会使用磁盘作为虚拟内存,影响Redis性能。
设置了maxmemory之后,当内存到达限制时,会触发内存淘汰机制,从而将内存回收。
2) 使用异步删除
Redis 4.0之后,新增了一个异步删除key的功能lazy free,即使用子线程来进行key删除,避免对主线程的阻塞。
3) 根据慢查询日志进行优化
Redis提供了slowlog功能,可以通过查看慢查询日志执行相应的优化。
4) 选择合适的持久化策略
除了RDB和AOF之外,在Redis 4.0版本之后还可以选择混合持久化策略;
在不需要持久化数据时,可以考虑关闭持久化。
5) 使用分布式架构增加读写速度
首选Redis Cluster集群模式的实现方案。
(2) 从客户端考虑:
1) 合理设置key的过期时间
避免频繁触发内存淘汰策略,同时要避免大量key失效可能会导致的缓存雪崩。
2) 合理选择键名长度和避免使用大key
键名太长会浪费资源,太短会导致可读性太差。
大key不是指长键名,而是指key对应的value过大。 由于Redis是单线程的,前边任务完不成,后边的处理不了,所以如果value过大,会导致性能较差,同时也会导致分布式架构中的内存数据和CPU的不平衡。
那么如何判断哪些是大key呢?
可以通过Redis自带的命令或其他方式来判断,可以参考这篇文章。
那么如果对大key进行优化呢?
可以对大key进行分割,拆成多个小key。
3) 使用Pipeline批量操作数据
使用管道可以进行批量操作,提高性能。
4) 使用连接池
使用连接池可以避免频繁的创建和销毁Redis连接。
更多关于Redis调优的内容可以查看这篇文章。