Redis串讲
Redis串讲
什么是Redis?
Redis是一个高性能的非关系型的键值对数据库,使用C编写实现的。与传统的数据库不同的是Redis是存在内存中的,所以读写速度非常快,每秒可以处理超过10万次的读写操作,这也是Redis常常被用作缓存的原因。
Redis的优缺点?
-
优点:
-
读写性能好,读的速度可达110000次/s,写的速度可达81000次/s。
-
支持数据持久化,有AOF和RDB两中持久化方式 数据结构丰富,支持String、List、Set、Hash等结构
-
支持事务,Redis所有的操作都是原子性的,并且还支持几个操作合并后的原子性执行,原子性指操作要么成功执行,要么失败不执行,不会执行一部分。
-
支持主从复制,主机可以自动将数据同步到从机,进行读写分离。
-
-
缺点:
-
因为Redis是将数据存到内存中的,所以会受到内存大小的限制,不能用作海量数据的读写
-
Redis不具备自动容错和恢复功能,主机或从机宕机会导致前端部分读写请求失败,需要重启机器或者手动切换前端的IP才能切换
-
Redis为什么这么快?为什么Redis6.0之前采用单线程而不采用多线程呢?
Redis为什么这么快主要有以下几个原因:
- 运行在内存中
- 数据结构简单
- 使用多路IO复用技术
- 单线程实现,单线程避免了线程切换、锁等造成的性能开销。
Redis6.0之前是单线程的,为什么Redis6.0之前采用单线程而不采用多线程呢?
简单来说,就是Redis官方认为没必要,单线程的Redis的瓶颈通常在CPU的IO,而在使用Redis时几乎不存在CPU成为瓶颈的情况。使用Redis主要的瓶颈在内存和网络,并且使用单线程也存在一些优点,比如系统的复杂度较低,可为维护性较高,避免了并发读写所带来的一系列问题。
Redis6.0之后为什么引入了多线程?
Redis的瓶颈在内存和网络,Redis6.0引入多线程主要是为了解决网路IO读写这个瓶颈,执行命令还是单线程执行的,所以也不存在线程安全问题。
Redis6.0默认是否开启了多线程呢?
默认是没有开启的,如需开启,需要修改配置文件redis.conf:io-threads-do-reads no,no改为yes
Redis的数据类型有哪些?
Redis的常见的数据类型有String、Hash、Set、List、ZSet。还有三种不那么常见的数据类型:Bitmap、HyperLogLog、Geospatial。
Bitmap:位图,是一个以位为单位的数组,数组中只能存储1或0,数组的下标在Bitmap中叫做偏移量。Bitmap实现统计功能,更省空间。面试中常问的布隆过滤器就有用到这种数据结构,布隆过滤器可以判断出哪些数据一定不在数据库中,所以常被用来解决Redis缓存穿透问题。
Hyperloglog:HyperLogLog 是一种用于统计基数的数据集合类型,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。HyperLogLog 的优点是,在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定 的、并且是很小的。缺点是 HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。常见的应用场景:统计网站的UV
Geospatial:主要用于存储地理位置信息,常用于定位附近的人,打车距离的计算等。
Redis的数据结构有哪些?
Redis的数据结构有简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表等。
简单动态字符串:大家都知道,Redis的底层是用C语言编写,但Redis并没有直接使用C语言传统的字符串表示,而是构建了一种名为简单动态字符串的抽象类型。
链表:链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。链表是列表的底层实现之一。
字典:字典,又称为符号表(symbol table)、关联数组(associativearray)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。
整数集合: 整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
压缩列表(ziplist):压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。
对象:可能看到这里,很多人在想Redis的数据结构和数据类型的区别,其实上面介绍的是Redis的底层数据结构,但Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构,是不是这就和前面对上了。
看到这里很多人会好奇,为什么不直接使用这些底层数据结构,而是要创建对象系统。对象系统主要有以下优点:
- 通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
- 我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
- 实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放,了解Java虚拟机的垃圾回收机制看到这里是不是很熟悉。
- redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。 对象这部分占了比较大的篇幅,其实面试中问的也不多,但为了更方便理解,介绍地多些。顺便看下这些底层数据结构和对象系统的对应关系。
对象这部分占了比较大的篇幅,其实面试中问的也不多,但为了更方便理解,介绍地多些。顺便看下这些底层数据结构和对象系统的对应关系。
最后介绍下面试中常问的跳跃表。
跳跃表(skiplist):跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。跳跃表作是序集合键的底层实现之一。
和链表、字典等数据结构被广泛地应用在Redis内部不同,Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构,除此之外,跳跃表在Redis里面没有其他用途。
跳跃表本质上采用的是一种空间换时间的策略,是一种可以可以进行二分查找的有序链表,跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
这是一个原始的有序列表,时间复杂度为O(n)。
为了提高查找效率,可以对链表建立一级索引,如下图,在之前找到11这个元素需要遍历6个节点,现在需要5个。链表越长,效率提升越明显。
为了继续提高查找效率可以继续增加索引
对于理想的跳表,每向上一层索引节点数量都是下一层的1/2,跳表的时间复杂度为o(logn),空间复杂度为o(n),虽然是空间换时间的策略,这里举例存储的只是数字,如果是存储比较大的对象,浪费的空间就不值得一提了,因为索引结点只需要存储关键值和几个指针,并不需要存储对象。
跳表相比于红黑树的优点(redis为什么用跳表不同红黑树):
- 内存占用更少,自定义参数化决定使用多少内存
- 查询性能至少不比红黑树差
- 简单更容易实现和维护
最后,说下Redis中的跳跃表和普通的跳跃表有什么区别?
- Redis中的跳跃表分数(score)允许重复,即跳跃表的key允许重复,如果分数重复,还需要根据数据内容来进字典排序。普通的跳跃表是不支持的。
- 第1层链表不是一个单向链表,而是一个双向链表。这是为了方便以倒序方式获取一个范围内的元素。
- 在Redis的跳跃表中可以很方便地计算出每个元素的排名。
一个字符串类型的值能存储最大容量是多少?
一个字符串类型键允许存储的数据的最大容量是512MB
Redis的应用场景有哪些?
- 缓存:Redis基于内存,读写速度非常快,并且有键过期功能和键淘汰策略,可以作为缓存使用。
- 排行榜:Redis提供的有序集合可以很方便地实现排行榜。
- 分布式锁:Redis的setnx功能来实现分布式的锁。
- 社交功能:实现共同好友、共同关注等 计数器:通过String进行自增自减实现计数功能
- 消息队列:Redis提供了发布、订阅、阻塞队列等功能,可以实现一个简单的消息队列。
Redis键的过期删除策略
常见的过期删除策略是惰性删除、定期删除、定时删除。
- 惰性删除:只有访问这个键时才会检查它是否过期,如果过期则清除。优点:最大化地节约CPU资源。缺点:如果大量过期键没有被访问,会一直占用大量内存。
- 定时删除:为每个设置过期时间的key都创造一个定时器,到了过期时间就清除。优点:该策略可以立即清除过期的键。缺点:会占用大量的CPU资源去处理过期的数据。
- 定期删除:每隔一段时间就对一些键进行检查,删除其中过期的键。该策略是惰性删除和定时删除的一个折中,既避免了占用大量CPU资源又避免了出现大量过期键不被清除占用内存的情况。
Redis中同时使用了惰性删除和定期删除两种。
Redis的内存淘汰机制是什么样的?
Redis是基于内存的,所以容量肯定是有限的,有效的内存淘汰机制对Redis是非常重要的。
当存入的数据超过Redis最大允许内存后,会触发Redis的内存淘汰策略。在Redis4.0前一共有6种淘汰策略。
- volatile-lru:当Redis内存不足时,会在设置了过期时间的键中使用LRU算法移除那些最少使用的键。
- volatile-ttl:从设置了过期时间的键中移除将要过期的
- volatile-random:从设置了过期时间的键中随机淘汰一些
- allkeys-lru:当内存空间不足时,根据LRU算法移除一些键
- allkeys-random:当内存空间不足时,随机移除某些键
- noeviction:当内存空间不足时,新的写入操作会报错
前三个是在设置了过期时间的键的空间进行移除,后三个是在全局的空间进行移除
在Redis4.0后可以增加两个
- volatile-lfu:从设置过期时间的键中移除一些最不经常使用的键(LFU算法:Least Frequently Used))
- allkeys-lfu:当内存不足时,从所有的键中移除一些最不经常使用的键 这两个也是一个是在设置了过期时间的键的空间,一个是在全局空间。
什么是Redis的持久化?
因为Redis是基于内存的,为了让防止一些意外情况导致数据丢失,需要将数据持久化到磁盘上。
Redis常见的持久化机制有哪些?有什么有优缺点?
Redis提供了两种不同的持久化方式,一种是RDB,一种是AOF。
RDB
RDB是redis默认的持久化方式,按照一定的时间间隔将内存的数据以快照的形式保存到硬盘,恢复时是将快照读取到内存中。RDB持久化实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。如下图
优点:
- 适合对大规模的数据恢复,比AOF的启动效率高
- 只有一个文件 dump.rdb,方便持久化
- 性能最大化,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
缺点:
- 数据安全性低,在一定间隔时间内做一次备份,如果Redis突然宕机,会丢失最后一次快照的修改
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优点:
- 具备更高的安全性,Redis提供了3种同步策略,分别是每秒同步、每修改同步和不同步。相比RDB突然宕机丢失的数据会更少,每秒同步会丢失一秒种的数据,每修改同步会不会丢失数据。
- 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。
- AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作,可以通过该文件完成数据的重建。
缺点:
- 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据AOF选择同步策略的不同,效率也不同,但AOF在运行效率上往往会慢于RDB。
什么是Redis的事务
Redis的事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断,所以Redis事务是在一个队列中,一次性、顺序性、排他性地执行一系列命令。
Redis 事务的主要作用就是串联多个命令防止别的命令插队。
Redis事务的相关命令
Redis事务相关的命令主要有以下几种:
- DISCARD:命令取消事务,放弃执行事务队列内的所有命令,恢复连接为非 (transaction) 模式,如果正在使用 WATCH 命令监视某个(或某些) key,那么取消所有监视,等同于执行命令 UNWATCH 。
- EXEC:执行事务队列内的所有命令。
- MULTI:用于标记一个事务块的开始。
- UNWATCH:用于取消 WATCH命令对所有 key 的监视。如果已经执行过了EXEC或DISCARD命令,则无需再执行UNWATCH命令,因为执行EXEC命令时,开始执行事务,WATCH命令也会生效,而 DISCARD命令在取消事务的同时也会取消所有对 key 的监视,所以不需要再执行UNWATCH命令了
- WATCH:用于标记要监视的key,以便有条件地执行事务,WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。
Redis事务的特性,Redis事务执行的三个阶段
Redis事务的特性
- Redis事务不保证原子性,单条的Redis命令是原子性的,但事务不能保证原子性。
- Redis事务是有隔离性的,但没有隔离级别,事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。(顺序性、排他性)
- Redis事务不支持回滚,Redis执行过程中的命令执行失败,其他命令仍然可以执行。(一次性) Redis事务执行的三个阶段 开始事务(MULTI) 命令入列 执行事务(EXEC)
Redis事务执行的三个阶段
- 开始事务(MULTI)
- 命令入列
- 执行事务(EXEC)
Redis事务为什么不支持回滚?
在Redis事务中,命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令,是不支持回滚的。
主要原因有以下两点:
- Redis 命令只在两种情况失败:
- 语法错误的时候才失败(在命令输入的时候不检查语法)。
- 要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
- 因为不需要回滚,所以Redis内部实现简单并高效。
Redis并发竞争key问题应该如何解决?
Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:
- 乐观锁,watch 命令可以方便的实现乐观锁。watch 命令会监视给定的每一个key,当 exec 时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用
- 分布式锁,适合分布式场景
- 时间戳,适合有序场景,比如A想把key设置为1,B想把key设置为2,C想把key设置为3,对每个操作加上时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了
- 消息队列,串行化处理
说下什么是缓存雪崩、缓存穿透、缓存击穿,及它们的解决方案
下图是一个正常的系统架构图,其中缓存的作用是减轻数据库的压力,提升系统的性能,无论是缓存雪崩、缓存击穿还是缓存穿透都是缓存失效了导致数据库压力过大。
缓存雪崩
什么是缓存雪崩?
缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。
为什么会造成缓存雪崩?
一般来说,造成缓存雪崩主要有两种可能
- Redis宕机了
- 很多key采取了相同的过期时间
如何解决缓存雪崩?
- 为避免Redis宕机造成缓存雪崩,可以搭建Redis集群
- 尽量不要设置相同的过期时间,例如可以在原有的过期时间加上随机数
- 服务降级,当流量到达一定的阈值时,就直接返回“系统繁忙”之类的提示,防止过多的请求打在数据库上,这样虽然难用,但至少可以使用,避免直接把数据库搞挂
缓存击穿
什么是缓存击穿?
缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。
比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。
如何解决缓存击穿
- 考虑热点key不设置过期时间,避免key过期失效
- 加锁,如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库宕机,不过这样会导致系统的性能变差。
缓存穿透
什么是缓存穿透
缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。
如何解决缓存穿透
- 参数校验,例如可以对用户id进行校验,直接拦截不合法的用户的请求
- 布隆过滤器,布隆过滤器可以判断这个key在不在数据库中,特点是如果判断这个key不在数据库中,那么这个key一定不在数据库中,如果判断这个key在数据库中,也不能保证这个key一定在数据库中。就是会有少数的漏网之鱼,造成这种现象的原因是因为布隆过滤器中使用了hash算法,对key进行hash时,不同的key的hash值一定不同,但相同的hash的值不能说明这两个key相同。下面简单介绍下布隆过滤器,这个面试也常问。
布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。
布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一系列的hash算法计算,算出每个key的位置,并将该位置的值置为1,为了减少哈希冲突的影响,可以对每个key进行多次hash计算,如下图
现在,用户所有的请求都要经过布隆过滤器过滤一遍,如果只有用户请求的key的hash值都是1才可以通过,否则直接拦截,如下图
那使用布隆过滤器就可以完美解决问题了吗?当然没有,使用布隆过滤器解决缓存穿透问题的同时也带来了一些其他问题:
布隆过滤器存在误判的情况
布隆过滤器不支持删除,因为布隆过滤器中存的1可能涉及多个key,直接删除可能会影响到其他key,比如上图第四个位置的1就涉及两个key
如果数据库中数据更新同步到布隆过滤器时失败,布隆过滤器则会将本来正常的请求拦截住,这是非常致命的
先来看第一个问题,前面已经解释过了布隆过滤器存在误判的原因,就是不同的key的hash值可能相同。因为每个key要经过多次hash计算,恰好每次hash计算都和其他key的hash值相同的概率是很低的,有少数的漏网之鱼通过了布隆过滤器也不要紧,所以第一个问题不必担心。如果想要减少hash冲突导致的误判,可以适当增加key的hash次数。
第二个问题可以在布隆过滤器中以计数的方式存储,如下图
上图中位置4被key1和key2的hash值覆盖,则为2,如果删除key2,则为下图
第三个问题出现概率不大,如果这种问题对业务影响很大,可以考虑其他解决缓存穿透的方法。
如何保证缓存与数据库双写时的数据一致性?
这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点
首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性
- 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。强一致性对用户比较友好,但对系统性能影响比较大。
- 弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。
- 最终一致性:也是弱一致性的一种特殊形式,系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。
大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。
如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:
- 先更新数据库,后更新缓存
- 先更新缓存,后更新数据库 先更新数据库,后删除缓存
- 先删除缓存,后更新数据库
乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。下面介绍删除缓存的方案
先更新数据库,后删除缓存
这种方案也存在一个问题,如果更新数据库成功了,删除缓存时没有成功,那么后面每次读取缓存时都是错误的数据。
解决这个问题的办法是删除重试机制,常见的方案有利用消息队列和数据库的日志
利用消息队列实现删除重试机制,如下图
步骤在图中写的已经比较清除了,这里简单说下为什么使用消息队列,消息队列可以保证写到队列中的消息在成功消费之前不会消失,并且在第4步中获取消息时只有消费成功才会删除消息,否则会继续投递消息给应用程序,符合消息重试的要求。
但这个方案也有一些缺点,比如系统复杂度高,对业务代码入侵严重,这时可以采用订阅数据库日志的方法删除缓存。如下图
先删除缓存,后更新数据库
这种方案也存在一些问题,比如在并发环境下,有两个请求A和B,A是更新操作,B是查询操作
- 假设A请求先执行,会先删除缓存中的数据,然后去更新数据库
- B请求查询缓存发现为空,会去查询数据库,并把这个值放到缓存中
- 在B查询数据库时A还没有完全更新成功,所以B查询并放到缓存中的是旧的值,并且以后每次查询缓存中的值都是错误的旧值
这种情况的解决方法通常是采用延迟双删,就是为保证A操作已经完成,最后再删除一次缓存
逻辑很简单,删除缓存后,休眠一会儿再删除一次缓存,虽然逻辑看起来简单,但实现起来并不容易,问题就出在延迟时间设置多少合适,延迟时间一般大于B操作读取数据库+写入缓存的时间,这个只能是估算,一般可以考虑读业务逻辑数据的耗时 + 几百毫秒。
在实际应用中,还是先更新数据库后删除缓存这种方案用的多些。
需要注意的是,无论哪种方案,如果数据库采取读写分离+主从复制延迟的话,即使采用先更新数据库后删除缓存也会出现类似先删除缓存后更新数据库中出现的问题,举个例子
- A操作更新主库后,删除了缓存
- B操作查询缓存没有查到数据,查询从库拿到旧值
- 主库将新值同步到从库
- B操作将拿到的旧值写入缓存
这就造成了缓存中的是旧值,数据库中的是新值,解决方法还是上面说的延迟双删,延迟时间要大于主从复制的时间
Redis如何实现大量数据插入?
这个问题在Redis的官方文档给出了答案,从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。
具体可以看官网的详细解释,https://www.redis.com.cn/topics/mass-insert.html
如何通过Redis实现异步队列?
主要有两种方式
第一种是使用List作为队列,通过RPUSH生产消息, LPOP消费消息
存在的问题:如果队列是空的,客户端会不停的pop,陷入死循环
解决方法:
- 当lpop没有消息时,可以使用sleep机制先休眠一段时间,然后再检查有没有消息。
- 可以使用blpop命令,在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。这种做法的缺点是只能提供一个消费者消费
第二种方法是pub/sub主题订阅模式,发送者(pub)发送消息,订阅者(sub)接收消息
存在的问题:消息的发布是无状态的,无法保证到达,如果订阅者在发送者发布消息时掉线,之后上线也无法接收发布者发送的消息
解决方法:使用消息队列
如何通过Redis实现延时队列?
先说下延时队列的使用场景:
- 常见的微信红包场景,A给B发红包,B没有收,1天后钱会退回原账户
- 电商的订单支付场景,订单在半小时内未支付会自动取消
上述场景可以通过定时任务采用数据库/非关系型数据库轮询方案或延迟队列,现主要介绍下Redis实现的延迟队列
可以通过Redis的zset命令实现延迟队列,ZSET是Redis的有序集合,通过zadd score1 value1命令向内存中生产消息,并利用设置好的时间戳作为score进行排序,然后通过zrangebysocre 查询符合条件的所有待处理的任务,循环执行,也可以zrangebyscore key min max withscores limit 0 1 查询最早的一条任务,来进行消费,如下图
Redis回收使用什么算法?
Redis回收使用LRU算法和引用计数法
LRU算法很常见,在学习操作系统时也经常看到,淘汰最长时间没有被使用的对象,LRU算法在手撕代码环节也经常出现,要提前背熟
引用计数法在学习JVM中也见过的,对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,当对象被一个新程序使用时,它的引用计数值会被增1,当对象不再被一个程序使用时,它的引用计数值会被减1,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,简单来说就是淘汰使用次数最少的对象(LFU算法)
Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?
可以使用Redis的KEYS命令,用于查找所有匹配给定模式 pattern 的 key ,虽然时间复杂度为O(n),但常量时间相当小。
注意: 生产环境使用 KEYS命令需要非常小心,在大的数据库上执行命令会影响性能,KEYS指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个命令适合用来调试和特殊操作,像改变键空间布局。
不要在你的代码中使用 KEYS 。如果你需要一个寻找键空间中的key子集,考虑使用 SCAN 或 sets。
Redis集群的实现方案有哪些?
在说Redis集群前,先说下为什么要使用Redis集群,Redis单机版主要有以下几个缺点:
- 不能保证数据的可靠性,服务部署在一台服务器上,一旦服务器宕机服务就不可用。
- 性能瓶颈,内存容量有限,处理能力有限
Redis集群就是为了解决Redis单机版的一些问题,Redis集群主要有以下几种方案
- Redis 主从模式
- Redis 哨兵模式
- Redis 自研
- Redis Cluster
下面对这几种方案进行简单地介绍:
Redis主从模式
Redis单机版通过RDB或AOF持久化机制将数据持久化到硬盘上,但数据都存储在一台服务器上,并且读写都在同一服务器(读写不分离),如果硬盘出现问题,则会导致数据不可用,为了避免这种问题,Redis提供了复制功能,在master数据库中的数据更新后,自动将更新的数据同步到slave数据库上,这就是主从模式的Redis集群,如下图
主从模式解决了Redis单机版存在的问题,但其本身也不是完美的,主要优缺点如下:
优点:
- 高可靠性,在master数据库出现故障后,可以切换到slave数据库
- 读写分离,slave库可以扩展master库节点的读能力,有效应对大并发量的读操作 缺点: 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低
缺点:
- 不具备自动容错和恢复能力,主节点故障,从节点需要手动升为主节点,可用性较低
Redis 哨兵模式
为了解决主从模式的Redis集群不具备自动容错和恢复能力的问题,Redis从2.6版本开始提供哨兵模式
哨兵模式的核心还是主从复制,不过相比于主从模式,多了一个竞选机制(多了一个哨兵集群),从所有从节点中竞选出主节点,如下图
从上图中可以看出,哨兵模式相比于主从模式,主要多了一个哨兵集群,哨兵集群的主要作用如下:
- 监控所有服务器是否正常运行:通过发送命令返回监控服务器的运行状态,处理监控主服务器、从服务器外,哨兵之间也相互监控。
- 故障切换:当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换master。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。
哨兵模式的优缺点:
优点:
- 哨兵模式是基于主从模式的,解决可主从模式中master故障不可以自动切换故障的问题。
缺点:
- 浪费资源,集群里所有节点保存的都是全量数据,数据量过大时,主从同步会严重影响性能
- Redis主机宕机后,投票选举结束之前,谁也不知道主机和从机是谁,此时Redis也会开启保护机制,禁止写操作,直到选举出了新的Redis主机。
- 只有一个master库执行写请求,写操作会单机性能瓶颈影响
Redis 自研
哨兵模式虽然解决了主从模式存在的一些问题,但其本身也存在一些弊端,比如数据在每个Redis实例中都是全量存储,极大地浪费了资源,为了解决这个问题,Redis提供了Redis Cluster,实现了数据分片存储,但Redis提供Redis Cluster之前,很多公司为了解决哨兵模式存在的问题,分别自行研发Redis集群方案。
客户端分片
客户端分片是把分片的逻辑放在Redis客户端实现,通过Redis客户端预先定义好的路由规则(使用哈希算法),把对Key的访问转发到不同的Redis实例中,查询数据时把返回结果汇集。如下图
客户端分片的优缺点:
优点:Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强。
缺点:
- 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
- 运维成本比较高,集群的数据出了任何问题都需要运维人员和开发人员一起合作,减缓了解决问题的速度,增加了跨部门沟通的成本。
- 在不同的客户端程序中,维护相同的路由分片逻辑成本巨大。比如:java项目、PHP项目里共用一套Redis集群,路由分片逻辑分别需要写两套一样的逻辑,以后维护也是两套。
代理分片
客户端分片的最大问题就是服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。
为了解决这个问题,代理分片出现了,代理分片将客户端分片模块单独分了出来,作为Redis客户端和服务端的桥梁,如下图
代理模式的优点:解决了服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整的问题。缺点是由于Redis客户端的每个请求都经过代理才能到达Redis服务器,这个过程中会产生性能损失。
常见的代理分片有Twitter开源的Redis代理Twemproxy和豌豆荚自主研发的Codis
Redis Cluster
前面介绍了为了解决哨兵模式的问题,各大企业提出了一些数据分片存储的方案,在Redis3.0中,Redis也提供了响应的解决方案,就是Redist Cluster。
Redis Cluster是一种服务端Sharding技术,Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。
什么是Redis哈希槽呢?本来不想详细介绍这个的,但面试确实经常问,还是简单说一下,
在介绍slot前,要先介绍下一致性哈希(客户端分片经常会用的哈希算法),那这个一致性哈希有什么用呢?其实就是用来保证节点负载均衡的,那么多主节点,到底要把数据存到哪个主节点上呢?就可以通过一致性哈希算法要把数据存到哪个节点上。
一致性哈希
下面详细说下一致性哈希算法,首先就是对key计算出一个hash值,然后对2^32取模,也就是值的范围在[0, 2^32 -1],一致性哈希将其范围抽象成了一个圆环,使用CRC16算法计算出来的哈希值会落到圆环上的某个地方。
现在将Redis实例也分布在圆环上,如下图
假设A、B、C三个Redis实例按照如图所示的位置分布在圆环上,通过上述介绍的方法计算出key的hash值,发现其落在了位置E,按照顺时针,这个key值应该分配到Redis实例A上。
如果此时Redis实例A挂了,会继续按照顺时针的方向,之前计算出在E位置的key会被分配到RedisB,而其他Redis实例则不受影响。
但一致性哈希也不是完美的,主要存在以下问题:当Redis实例节点较少时,节点变化对整个哈希环中的数据影响较大,容易出现部分节点数据过多,部分节点数据过少的问题,出现数据倾斜的情况,如下图,数据落在A节点和B节点的概率远大于B节点
为了解决这种问题,可以对一致性哈希算法引入虚拟节点(A#1,B#1,C#1),如下图
那这些虚拟节点有什么用呢?每个虚拟节点都会映射到真实节点,例如,计算出key的hash值后落入到了位置D,按照顺时针顺序,应该落到节点C#1这个虚拟节点上,因为虚拟节点会映射到真实节点,所以数据最终存储到节点C。
虚拟槽
在Redis Cluster中并没有使用一致性哈希,而引进了虚拟槽。虚拟槽的原理和一致性哈希很像,Redis Cluster一共有2^14(16384)个槽,所有的master节点都会有一个槽范围比如0~1000,槽数是可以迁移的。master节点的slave节点不分配槽,只拥有读权限,其实虚拟槽也可以看成一致性哈希中的虚拟节点。
虚拟槽和一致性哈希算法的实现上也很像,先通过CRC16算法计算出key的hash值,然后对16384取模,得到对应的槽位,根据槽找到对应的节点,如下图。
使用虚拟槽的好处:
- 更加方便地添加和移除节点,增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了,当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了,不需要停掉Redis任何一个节点的服务(采用一致性哈希需要增加和移除节点需要rehash)
Redis Cluster 结构
上面介绍了Redis Cluster如何将数据分配到合适的节点,下面来介绍下Redis Cluster结构,简单来说,Redis Cluster可以看成多个主从架构组合在一起,如下图
上图看起来比较乱,其实很好理解,上图中一个Redis Cluster有两组节点组成(官方推荐,一般至少三主三从六个节点,画多个节点看起来太乱,所以上图只画了两个主节点),每组节点可以看成一个主从模式,并且每组节点负责的slot不同(假设有4个slot,A组节点负责第1个和第二个slot,B组节点负责第3个和第4个,其中master节点负责写,slave节点负责读)
上图中共有三种线
主备复制的线很好理解,就和主从模式一样,在master库中的数据更新后,自动将更新的数据同步到slave库上
对外服务的线也很好理解,就是外部在对Redis进行读取操作,访问master进行写操作,访问slave进行读操作
Redis Bus的作用相对复杂些,这里简单说下,
首先要知道Redis Cluster是一个去中心化的架构,不存在统一的配置中心,Redis Cluster中的每个节点都保存了集群的配置信息,在Redis Cluster中,这个配置信息通过Redis Cluster Bus进行交互,并最后达成一致性。
配置信息的一致性主要依靠PING/PONG,每个节点向其他节点频繁的周期性的发送PING/PONG消息。对于大规模的集群,如果每次PING/PONG 都携带着所有节点的信息,则网络开销会很大。此时Redis Cluster 在每次PING/PONG,只包含了随机的一部分节点信息。由于交互比较频繁,短时间的几次交互之后,集群的状态也会达成一致。
当Cluster 结构不发生变化时,各个节点通过gossip 协议(Redis Cluster各个节点之间交换数据、通信所采用的一种协议)在几轮交互之后,便可以得知Cluster的结构信息,达到一致性的状态。但是当集群结构发生变化时(故障转移/分片迁移等),优先得知变更的节点会将自己的最新信息扩散到Cluster,并最终达到一致。
其实,上面说了半天也不太容易理解,简单来说Redis Bus是用于节点之间的信息交路,交互的信息有以下几个:
- 数据分片(slot)和节点的对应关系(对应上图中的slot1和slot2在masterA节点上,就是要知道哪个slot在哪个节点上)
- 集群中每个节点可用状态(不断向其他节点发消息看看你挂了没)
- 集群结构发生变更时,通过一定的协议对配置信息达成一致。数据分片的迁移、主备切换、单点master的发现和其发生主备关系变更等,都会导致集群结构变化。(发现有的节点挂了或者有新的节点加进来了,赶紧和其他节点同步信息)
Redis主从架构中数据丢失吗
Redis主从架构丢失主要有两种情况
- 异步复制同步丢失
- 集群产生脑裂数据丢失
下面分别简单介绍下这两种情况:
异步复制同步丢失:
Redis主节点和从节点之间的复制是异步的,当主节点的数据未完全复制到从节点时就发生宕机了,master内存中的数据会丢失。
如果主节点开启持久化是否可以解决这个问题呢?
答案是否定的,在master 发生宕机后,sentinel集群检测到主节点发生故障,重新选举新的主节点,如果旧的主节点在故障恢复后重启,那么此时它需要同步新主节点的数据,此时新的主节点的数据是空的(假设这段时间中没有数据写入)。那么旧主机点中的数据就会被刷新掉,此时数据还是会丢失。
集群产生脑裂:
简单来说,集群脑裂是指一个集群中有多个主节点,像一个人有两个大脑,到底听谁的呢?
例如,由于网络原因,集群出现了分区,master与slave节点之间断开了联系,哨兵检测后认为主节点故障,重新选举从节点为主节点,但主节点可能并没有发生故障。此时客户端依然在旧的主节点上写数据,而新的主节点中没有数据,在发现这个问题之后,旧的主节点会被降为slave,并且开始同步新的master数据,那么之前的写入旧的主节点的数据被刷新掉,大量数据丢失。
Redis集群的主从复制过程是什么样的?
主从复制主要有以下几步:
- 设置服务器的地址和端口号
- 建立套接字连接(建立主从服务器之间的连接)
- 发送PING命令(检验套接字是否可用)
- 身份验证 同步(从主库向从库同步数据,分为全量复制和部分复制)
- 命令传播(经过上面同步操作,此时主从的数据库状态其实已经一致了,许主服务器马上就接受到了新的写命令,执行完该命令后,主从的数据库状态又不一致。数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性)
简单解释下全量复制和部分复制:
在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。
- 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
- 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
Redis是如何保证主从服务器一致处于连接状态以及命令是否丢失?
命令传播阶段,从服务器会利用心跳检测机制定时的向主服务发送消息。
因为网络原因在主从复制过程中停止复制会怎么样?
如果出现网络问题断开,会自动重连,并且支持断点续传,接着上次复制的地方继续复制,而不是重新复制一份。
下面说下其中的实现细节,首先需要了解replication buffer和replication backlog
replication buffer:主库连接的每一个从库的对应一个 replication buffer,主库执行完每一个操作命令后,会将命令分别写入每一个从库所对应的 replication buffer
replication backlog:replication backlog 是一个环形区域,大小可以通过 repl-backlog-size参数设置,并且和 replication buffer不同的是,一个主库中只有一个 replication backlog。
主库会通过 master_repl_offset 记录写入的位置,从库会通过 slave_repl_offset 记录自己读取的位置,这里的位置也叫做偏移量。
刚开始复制数据的时候,上述两者的值相同,且都位于初始位置。每当主库向 replication backlog 写入一个操作,master_repl_offset 就会增加 1,从库复制完操作命令后,slave_repl_offset 也会增加 1。
正常情况下,slave_repl_offset 会跟着 master_repl_offset 变化,两者保持一个小的差距,或者相等。
如果主从库之间的网络连接中断,从库便无法继续复制主库的操作命令,但是主库依然会向 replication backlog 中写入操作命令。
当网络恢复之后,从库会继续向主库请求同步数据,主库通过slave_repl_offset知道从库复制操作命令的位置。这个时候,主库只需要把 master_repl_offset 和 slave_repl_offset 之间的操作命令同步给从库就可以了。
但是前面提到 replication backlog 是一个环形结构,如果网络中断的时间过长,随着主库不断向其中写入操作命令,master_repl_offset 不断增大,就会有从库没有复制的操作命令被覆盖。如果出现这种情况,就需要重新进行全量复制了。
为了避免全量复制的情况,可以通过修改 repl-backlog-size 参数的值,为 replication backlog 设置合适的大小。
这个值需要结合实际情况来设置,具体思路是:从命令在主库中产生到从库复制完成所需要的时间为 t,每秒钟产生的命令的数量为 c,命令的大小为 s,这个值不能低于他们的乘积。考虑到突发的网络压力以及系统运行过程中可能出现的阻塞等情况,应该再将这个值乘以 2 或者更多。
Redi集群最大的节点个数是多少?为什么?
16384个
Redis作者的回答主要说了两点:
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 *8 (8 bit)* 1024(1k) = 16K
),也就是说使用2k的空间创建了16k的槽数。虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 *8 (8 bit)* 1024(1k) =65K
),也就是说需要8k的心跳包,作者认为这样做不太值得
由于其他设计折衷,一般情况下一个redis集群不会有超过1000个master节点
Redis集群是如何选择数据库的?
Redis 集群目前无法做数据库选择,默认在 0 数据库。
Redis高可用方案如何实现?
常见的Redis高可用方案有以下几种:
- 数据持久化
- 主从模式
- Redis 哨兵模式
- Redis 集群(自研及Redis Cluster)
Redis分区的作用是什么?Redis分区有哪些实现方案?
Redis分区的作用是什么?
- 扩展数据库容量,可以利用多台机器的内存构建更大的数据库
- 扩展计算能力,分区可以在多核和多计算机之间弹性扩展计算能力,在多计算机和网络适配器之间弹性扩展网络带宽
Redis分区有哪些实现方案?
在介绍Redis集群的实现方案时已经介绍过了客户端分区和代理分区,常见的Redis分区方案主要有以下三种:
- 客户端分区:客户端决定数据被存到哪个Redis节点或者从哪个节点读取
- 代理分区:客户端将请求发送到代理,而不是直接发送到Redis节点,代理根据分区策略将请求发送到Redis节点上
- 查询路由:客户端随机请求任意一个Redis节点,这个Redis节点将请求转发到正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,并不是直接将请求从一个Redis节点转发到另一个Redis节点,而是在客户端的帮助下直接重定向到正确的redis节点
Redis分区的缺点?
- 不支持多个键的操作,例如不能操作映射在两个Redis实例上的两个集合的交叉集。(其实可以做到这一点,但是需要间接的解决)
- Redis不支持多个键的事务
- Redis是以键来分区,因此不能使用单个大键对数据集进行分片,例如一个非常大的有序集
- 数据的处理会变得复杂,比如你必须处理多个RDB和AOF文件,在多个实例和主机之间持久化你的数据
- 添加和删除节点也会变得复杂,例如通过在运行时添加和删除节点,Redis集群通常支持透明地再均衡数据,但是其他系统像客户端分区或者代理分区的特性就不支持该特性。不过Pre-sharding(预分片)可以在这方面提供帮助。