Redis常见问题
Redis是什么?
Redis是一种基于内存的数据库,读写速度非常快;Redis提供了丰富的数据类型,如string、list、hash等,并且对这些类型的操作都是原子的;Redis支持事务、持久化、Lua脚本、多种集群方案等特性;Redis常用于缓存、消息队列、分布式锁等场景。
Redis和Memcached的区别?
共同点:1. 都是基于内存的数据库;2. 都有过期策略;3. 性能都非常高。
不同点:1. Redis支持丰富的数据类型,而Memcached只支持最基本的键值类型;2. Redis支持持久化;3. Redis原生支持集群,而Memcached需要依赖客户端实现;4. Redis支持Lua脚本、事务等。
为什么用Redis作为MySQL的缓存?
主要是因为Redis既是高性能的也是高并发的。Redis的高性能源于它是基于内存的,具有很快的数据读写速度;Redis单设备的QPS是MySQL的10倍左右(10W+)。
Redis的数据结构有哪些?
String:由简单动态字符串SDS实现,相较于C语言的字符串,不仅可以存文本还可以存二进制;获取长度是O(1)时间复杂度;由于存在剩余内存空间检查,拼接也不会造成缓冲区溢出。
List:由双向链表或压缩列表(连续内存,无需链接,空间占用不大时使用)实现,Redis3.2后统一由quicklist(整体双向链表,节点内压缩列表)实现。
Hash:由哈希表或压缩列表实现。
Set:由哈希表或整数集合(连续内存,元素为整数,数量<512)。
Zset:由压缩列表(Redis3.2后改为packlist,避免连锁更新)或跳表实现。
渐进式Rehash:为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。在 rehash 期间,每次哈希表元素进行增删改查时,除了会执行对应的操作之外,还会顺序将key-value迁移到新hash表。
跳表:跳表是一种多层的有序列表,通过在节点上设置不同的层高,使得跳表进行操作时可跳过中间的一些可排除的节点,相较于顺序查找,可到达更远的节点。相比平衡树,从内存占用来说,跳表更加灵活;从范围查询来说,跳表比平衡树操作简单;从算法实现来说,跳表实现简单的多。
Redis是单线程吗?
Redis单线程是指接收客户端请求、解析请求、数据读写、发送结果给客户端这个过程是由一个线程完成的。
Redis是会启动后台线程来异步完成一些工作,包括处理关闭文件、AOF刷盘、异步释放Redis大内存等操作。
Redis单线程模型是怎样的?
Redis单线程模型的和兴是使用epoll完成的,epoll是一种I/O多路复用的手段,可以实现单线程同时处理多个网络连接。
Redis在初始化时,会创建epoll对象和监听套接字,然后调用epoll_ctl()
将监听套接字加入epoll,同时注册连接事件的处理函数,初始化后主线程进入事件循环函数:
- 首先检查发送队列任务并进行发送,如果本次未发送完则注册写事件处理函数,等
epoll_wait
发现可写后处理。 - 使用
epoll_wait
等待事件到来:- 连接事件:调用连接事件处理函数,使用
accept
获取连接套接字、调用epoll_ctl
将套接字加入epoll、同时注册读事件处理函数。 - 读事件:调用读事件处理函数,调用
read
接收数据、解析命令、处理命令、将客户端对象加入发送队列、执行结果加入到缓存等待发送。 - 写事件:调用写事件处理函数,调用
write
发送缓冲区内容、没写完则继续注册些时间处理函数。
- 连接事件:调用连接事件处理函数,使用
Redis单线程为什么这么快?
- 大部分操作在内存完成;
- 单线程模型避免了多线程竞争、切换代价,也没有死锁问题;
- I/O多路复用机制能够并发处理大量客户端请求,实现高并发。
Redis为什么使用单线程?
- CPU不是限制Redis性能的主要瓶颈,更多是受到内存大小和网络I/O速度的限制;
- 使用单线程可维护性更高,多线程引入程序执行顺序的不确定性,增加了系统复杂度,同时还存在线程切换代价。
Redis为什么引入多线程?
- 网络硬件的性能提升,使得Redis的性能瓶颈有时会出现在网络I/O处理上,因此采用多个I/O线程来处理网络I/O
- 但命令的执行仍然采用单线程处理;
- Redis6.0引入的多线程I/O特性对性能提升至少是一倍以上。
Redis如何实现持久化?
主要是通过AOF和RDB实现的:
- AOF:每执行一条写命令,就将命令以追加方式写入到AOF文件;
- RDB:将某个时刻的内存数据以二进制的形式写入磁盘。
AOF如何实现?
Redis在执行完一条写操作命令后,就会把该命令以追加方式写入AOF文件。先执行后记录是为了避免命令的二次检查、并且不阻塞当前写操作执行,然而存在数据丢失和阻塞其它操作的风险。
AOF回写策略?
- Always:每次将AOF日志回写到磁盘;
- EverySec:先将命令写入AOF的内核缓冲区,每隔一秒进行刷盘;
- No:只写入内核缓冲区,由操作系统决定刷盘时机。
AOF日志过大会怎样?
会触发AOF重写机制,读取当前数据库中的所有键值对,然后将每个键值对记录到新的AOF文件(后台子进程完成,利用写时复制机制实现内存快照),为了解决父子进程之间数据不一致的问题,设置了AOF重写缓冲区将重写期间产生的写操作缓存,在重写完成后追加到新的AOF文件。
RDB快照的实现?
通过save命令(主线程执行,会阻塞其它操作)和bgsave触发,bgsave会创建子进程利用写时复制机制获取并保存Redis内存快照,同时不会阻塞主线程的执行。
混合持久化?
RDB的优势是数据恢复速度快,但只保存快照由丢失大量数据的风险,并且保存RDB的频率不易控制。AOF的优点是丢失数据少,但数据恢复速度慢。
因此Redis将两种方式结合实现了混合持久化,在AOF重写日志时,会先将当前数据库内存快照以RDB方式写入AOF文件,重写缓冲区中的命令才会以追加的方式写入到AOF文件中。这样可以结合了RDB和AOF的优点,然而会降低AOF文件的可读性,并且不兼容Redis 4.0之前的版本。
Redis如何实现服务高可用?
高可用的Redis需要通过集群的方式冗余来实现,比如Redis的主从复制、哨兵模式、切片集群。
- 主从复制:Redis采用一主多从、读写分离的模式,主服务器上可以进行读写操作,从服务器只读,并接受主服务器同步过来的写操作命令(主从之间的命令复制时异步进行的,所以无法实现强一致性)
- 哨兵模式:哨兵用于监控主从服务器,并且提供主从节点故障转移。
- 切片集群模式:Redis缓存数据量过大,单机无法缓存时,就要使用Redis切片集群。该模式采用hash槽来处理数据和节点之间的映射关系,一个切片集群共有(16K)个哈希槽,放到心跳包中就是2K位,hash槽映射到具体的Redis节点主要通过平均分配和手动分配两种方式。
集群脑裂导致数据丢失?
主节点与其它节点失联,但和客户端正常通信,哨兵节点会重新选择新的主节点,原主节点恢复连接后,被降级为从节点,需要从新主节点重同步数据,这就会导致原主节点在失联期间与客户端做出的变更丢失。
解决方案:当主节点发现从节点下线/超时的总数大于阈值时,禁止写操作并返回错误到客户端。
Redis使用的过期删除策略?
Redis对于设置了过期事件的key,会将该key和过期时间存储与一个过期字典中。查询key时,会先查是否在过期字典,若不在则正常查询数据库,如果在则将过期事件与当前系统时间进行比对,未过期才返回。
Redis使用的过期删除策略是惰性删除+定期删除,惰性删除是指Redis不主动删除过期键,有访问发现key过期时才进行删除。定期删除是每隔一段时间随机从数据库取出一定数量的key进行检查,删除其中的过期key。
惰性删除对性能影响小,但会让过期键值浪费内存,定期删除需要占用较多资源,但能释放掉部分内存空间。
Redis持久化时对过期键如何处理?
- RDB生成:不存储过期键;
- RDB加载:主服务器不加载,从服务器加载(从服务器不进行过期检查);
- AOF写入:如果过期键未被删除,AOF保留,被删除后显示追加一条DEL命令;
- AOF重写:不写入过期键。
Redis主从模式对过期键如何处理?
从服务器不会进行过期扫面,从库对过期键的处理是被动的,主库在key到期时会向AOF文件增加DEL命令,并同步到所有从库,从库通过执行这一命令完成过期键的删除。
Redis内存满了会怎样?
Redis到达设定的最大运行内存,会触发内存淘汰机制:
- 不进行淘汰:Redis不再提供服务,返回错误;
- 在设置了过期时间的数据范围内淘汰:a. 随机淘汰;b. 选取多个优先淘汰其中更早过期的键值;c. LRU;d. LFU
- 在所有数据中淘汰:a. 随机淘汰;b. LRU;c. LFU
LRU和LFU的区别?
LRU根据最近访问时间来进行淘汰,一般使用链表存储,通过将新访问的节点移动到链表头,淘汰时淘汰链表尾来实现。Redis为了减少空间占用以及频繁移动链表节点的性能消耗,所以没有实现链表,而会记录每个数据的最后一次访问时间,随机取5个,淘汰其中最久未使用的。缺点是无法解决缓存污染,即只读取一次的大量数据会在留存较长时间。
LFU根据访问频率来淘汰,相较于LRU记录的不是最近访问时间而是数据的访问频次。
使用缓存的问题?
缓存雪崩:大量缓存数据同一时间失效,导致大量用户请求直接访问数据库。(解决:不设置过期或打散数据的失效时间)
缓存击穿:某个热点数据过期,直接访问数据库。(解决:设置互斥锁限制访问并发度、设置不过期、快过期时通知续期)
缓存穿透:请求的数据不在缓存,也不在数据库,从而绕过缓存访问数据库。(解决:在API入口校验参数、在缓存处设置空值、布隆过滤器)
常见的缓存更新策略?
Cache Aside(旁路缓存):写策略,先更新数据库数据,再删除缓存;读策略,命中缓存则返回,没有命中则从数据库读,然后写入缓存并返回用户。(先删除缓存会出现的问题:A需要更新数据库,A先删缓存,此时B读数据发现缓存没有,B读到数据库旧数据并更新到缓存,A再更新数据库,就会出现缓存里是旧数据,数据库是新数据的情况。先更新后删除可能导致的数据不一致:A读缓存未命中,读数据库,B此时将新值写入到数据库并删除缓存,A将读到的旧值写入到缓存,此时也会造成数据不一致,但由于写缓存要远快于写数据库,这种情况出现概率很低)这种更新策略适合读多写少的场景,因为频繁写入会让缓存数据频繁删除,影响缓存命中率(可以考虑在更新数据时也更新缓存但加锁/设置过期时间的方式来减轻并发更新缓存的影响。)
Read/Write Through(只读/只写):应用程序只和缓存交互,不在和数据库交互,而是由缓存和数据库交互,但分布式缓存往往不提供写入数据库和自动加载数据库中数据的功能。
Write Back(回写):在更新数据时只更新缓存,并为其设置脏标识,然后返回,并不更新数据库,只通过批量异步更新的方式更新。适合写多的场景,但无法保证数据库与缓存之间的强一致性,会有丢失数据的风险。
Redis如何实现延时队列?
通过Zset实现,在Score中存储延时的执行时间,使用ZADD生产数据,使用ZRANGEBYSCORE来进行消费。
Redis大key如何处理?
大key是指value占用空间大,大key会造成客户端阻塞、网络阻塞、阻塞工作线程、内存分布不均等问题,大key不能一下子删除,因为内存释放时,操作系统会将其加入空闲内存链表,以便于后续的管理和再分配,这一过程本身需要时间,并会阻塞当前应用程序。因此大key需要分批次删除,或者使用Redis4.0后的异步删除。
Redis的管道?
Pipeline是Redis提供的一种批处理技术,用于一次处理多个Redis命令,减少单命令执行方式命令间的网络等待。
Redis事务支持回滚吗?
Redis不支持回滚,Redis执行错误通常是因为编程错误,需要开发者自行解决,此外回滚这一复杂特性与Redis追求简单高效的设计主旨不符。
Redis实现分布式锁?
SET有NX参数实现,当key不存在时才会插入。
- 加锁:读取锁变量、检查锁变量、设置锁变量,SET NX操作可以保证这三步操作原子。(锁变量通常需要设置过期时间以防止死锁,其值需要能区分所来自的客户端,避免错误释放)
- 释放:检查值是否为当前客户端,是则删除,由于需要原子性,通常需要使用Lua脚本。
- 优点:性能高效,实现方便。
- 缺点:超时时间不好设置,主从复制时异步复制,不可靠
- RedLock:解决集群Redis分布式锁的可靠性,让客户端和多个独立的Redis节点依次请求加锁、超过半数成功才算加锁成功。