本文整理了 Redis 常见的经典面试题,「处女座笔记」「吐血推荐」「建议收藏」。
文章内容包含自己的理解,面试经验,也会参照最新的一些相关文章,但是解决了内容缺失、偷换概念、答非所问和内容冗余的Bug。不敢说这篇文章时最全最好的,但是我敢保证:“应对大厂面试,这一篇就足足足足足够了”。
更多 Redis 专题请转微博:关于:Redis 基础知识,集群原理和面试资料【篇】(专题汇总)
在项目中使用 Redis,主要是从两个角度去考虑:性能,并发。
- 高性能
与 MySQL 关系型数据库不同 ,Redis的数据是存在内存中,它的读写速度非常快,每秒可以处理超过10万次读写操作,让请求能够迅速响应。需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。
- 高并发
在高并发的场景下,如果所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 Redis 做一个缓冲操作,让请求先访问到 Redis,而不是直接访问数据库。
除了做缓存使用,Redis 还可以做分布式锁(setnx),并且支持 publish 和 subscribe 命令实现订阅和发布的功能,像 MQ 那样。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案,如:主从模式,哨兵模式,Redis Cluster 集群模式...
常用应用场景:缓存,排行榜,计数器应用,共享Session,分布式锁,社交网络,消息队列,位操作...
常见的缓存问题:缓存与DB双写不一致 、缓存并发竞争、缓存击穿、缓存穿透、缓存雪崩等问题。【具体解决方案下面有讲 ↓ ↓ ↓ 】
五种基本类型:String(字符串)、Hash(哈希字典)、List(列表)、Set(集合)、zset(有序集合)。
三种特殊类型:HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置)。
- Geospatial:Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
- HyperLogLog:用来做基数统计算法的数据结构,如:统计网站的UV。
- Bitmaps :用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于字符串类型实现的,可以把 bitmaps 成作一个以比特位为单位的数组,最大的好处就是节省空间。
此外,如果你还玩过 Redis Module,像 BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了,感兴趣的话赶紧百度科普吧。
Redis 常用命令:Redis 基本结构类型常用命令 - 菜鸟
面试官心理:
如果面试官感觉你是比较初级的同学,或者觉得你并没有什么项目优化经验,还真的可能问这类问题。主要目的,就是看你有没有真正了解过 Redis,是不是能用合适的数据结构去优化自己的业务,是不是就只会用 String、String、String。
面试题剖析:
- String:不必多说,最简单的类型,就是普通的 set 和 get,做简单的 K-V 缓存,貌似什么数据都可以存;
- Hash:类似 Map 的一种结构,一般存储可以结构化的数据,比如 Java 对象。特别注意:Hash 结构支持每次读写的时候,直接操作 Hash 里的某个字段,千万不要把 Map 都取出来操作,最后再全部存进去,新手很容易这么干,生产上极易出现内存溢出的问题!!(我身边就真的发生过这类的生产问题)
- List:有序列表,存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。可以通过 lrange 命令,读取某个闭区间内的元素,还可以基于 list 实现分页查询,这个是很棒的一个功能。基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。List 结构还能实现类似 MQ 的广播功能,,rpush 生产消息,lpop 消费消息。
- Set:无序集合,自动去重。分布式系统中,可以基于 redis 进行全局的 set 去重。除此之外,还可以玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁。
- Sorted Set:排序的 set,去重且可以排序,写进去的时候给一个分数,自动根据分数排序,然后根据分数取任意区间内的数据,用途也是相当广泛。
大量的 key 过期时间设置的过于集中,到过期的时间点,Redis 可能会出现短暂的卡顿现象,更严重的情况是,可能出现雪崩现象。
所以,一般需要在时间上加一个随机值,使得过期时间分散一些,还有,可以考虑少量的热点数据设置成永不过期。
必要时,要做好服务的熔断和降级准备,防止雪崩的情况,设计的宗旨就是:坚决不可以让关系型数据库承载高并发请求。
在 Redis 中,我们把访问频率高的 key,称为热点 key。热点 key 由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
- Redis 集群扩容:增加分片副本,均衡读流量;
- 将热 key 分散到不同的服务器中,防止单台服务并发过大;
- 使用多级缓存,即:JVM 本地缓存,接入层 Nginx 缓存,减少 Redis 的读请求。
使用 [ - keys 前缀* ] 指令可以扫出指定模式的 key 列表。
这个时候你要回答 Redis 关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令是基于游标的迭代器,可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
Redis 是基于内存的非关系型K-V数据库,既然它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis 提供了 RDB 和 AOF 两种持久化机制,即:把数据保存到磁盘。
RDB 持久化是 Redis 默认的持久化方式,指在指定的时间间隔内,将内存中的数据集快照写入磁盘中。执行完操作后,在指定目录下会生成一个 dump.rdb
文件,Redis 重启的时候,通过加载 dump.rdb
文件来恢复数据。
- 优点:适合大规模的数据恢复场景,如备份,全量复制等;
- 缺点:不够实时,没办法做到实时持久化/秒级持久化;
AOF(append only file) 持久化,采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行 AOF 文件中的命令来恢复数据,它主要解决数据持久化的实时性问题。
- 优点:弥补了RDB实时性的问题,数据的一致性和完整性更高;
- 缺点:AOF记录的内容越多,文件越大,数据恢复变慢;
生产上通常采用【AOF + RDB】的混合模式作为持久化方案。通过 aof-use-rdb-preamble 配置参数控制,yes则表示开启,no表示禁用,默认是禁用的,可以通过 config set 修改。
bgsave 做镜像全量持久化,AOF 做增量持久化。因为 bgsave 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在 redis 实例重启时,优先使用 aof 来恢复内存的状态,如果没有 AOF 日志,就会使用 RDB 文件来恢复。
RDB 文件只用作后备用途,建议只在 slave 上持久化 RDB 文件,而且只要15分钟备份一次就够 了,只保留 save 900 1 这条规则。
告诉面试官,Redis 会定期做AOF 重写,压缩AOF 文件日志大小。
如果面试官不够满意,再拿出杀手锏答案:Redis4.0之后有了混合持久化的功能,将 bgsave的全量和 aof 的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。这个功能甚至很多面试官都不知道,他们肯定会对你刮目相看。
可以的,Redis提供两个指令生成 RDB,分别是 save 和 bgsave。
- 如果是save指令,会阻塞,因为是主线程执行的。
- 如果是bgsave指令,是fork一个子进程来写入RDB文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。
取决于AOF 日志 sync 属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s/次,这个时候最多就会丢失1s的数据。
- Redis 支持复杂的数据结构:
redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。
Redis 原生支持集群模式:
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
Memcached 在存储大数据的性能上更佳:
由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比memcached 性能更高,而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。
Redis 可以使用主从同步,从从同步。
第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
在 Redis 官方文档中有相关介绍,底层使用 TCP 发送 RESP 格式的协议。
RESP,英文全称是 Redis Serialization Protocol,它是专门为 Redis 设计的一套序列化协议。这个协议其实在 Redis1.2 版本时就已经出现了,但是到了 Redis 2.0 才最终成为 Redis 通讯协议的标准。
RESP 简单来说就是一套字符串解析规则,有点类似于 Json 中的大括号{}表示对象,方括号[ ]表示数组一样,都是用来解析字符串的。
RESP 协议主要优点是:实现简单、解析速度快、可读性好等。
官方文档里的 RESP 协议格式:
- 简单的字符串以"+" 开头;
- 错误以 " - " 开头;
- 整数以 ": " 开头;
- 大容量字符串以 " $ " 开头,紧接着是一个数字表示长度;
- 数组以 " * " 开头,也是紧随着一个数字,表示数组长度;
Redisson - 是一个高级的分布式协调 Redis 客服端,支持 Redis 多种连接模式。Redisson API 侧重于分布式开发,能帮助用户在分布式环境中轻松实现一些 Java 对象。
场景:分布式锁可能存在锁过期释放,但是业务没执行完的问题。
开源框架 Redisson 就解决了这个分布式锁问题:只要线程一加锁成功,就会启动一个watch dog
看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。
先拿 setnx() 来争抢锁,抢到之后,再用 expire() 给锁加一个过期时间防止锁忘记了释放。
为了防止在 setnx() 之后执行 expire() 之前,进程意外 crash 或者要重启维护,导致锁永远得不到释放的问题出现,可以使用 Lua 脚本同时把 setnx 和 expire 合成一条指令来使用。
具体讲解请转到博客:基于 Redis + Lua 脚本实现分布式锁,确保操作的原子性
一般使用 List 结构作为队列,rpush 生产消息,lpop 消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
List 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。
使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列,如:RabbitMQ,RocketMQ等。
使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
Pipeline(管道)可以一次性发送多条命令,并在执行完后一次性将结果返回,pipeline 通过减少客户端与 redis 的通信次数来实现降低往返延时时间,而且 Pipeline 实现的原理是队列,而队列的原理是时先进先出,这样就保证数据的顺序性。
使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS 峰值的一个重要因素是 pipeline 批次指令的数目,它对于提升峰值 QPS 非常有用。
Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
来看客户端与 Redis 的一次通信过程:
Redis 线程模型的特点,也解释了为什么效率这么高,支持高并发:
- 纯内存操作;
- 核心是基于非阻塞的 IO 多路复用机制;
- 单线程反而避免了多线程的频繁上下文切换问题;
Redis 是基于内存的,内存是很宝贵而且很有限的,不像磁盘一样巨大到可以做数据的持久化的程度。有同学说:“Redis不是可以做持久化的吗?”,同学要搞清楚啦,Redis的持久化,也是将数据刷到磁盘里保存。
既然内存是有限的,那么当内存空间不足的时候,就需要干掉不常用的数据,保留常用的数据,Redis 是这么干的:
Redis 过期策略是:定期删除 + 惰性删除。
所谓定期删除,指的是 Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
假设 Redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 Redis基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 Redis 是每隔 100ms 随机抽取一些key 来检查和删除的。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,Redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
假如大量的 key 没有设置过期时间,无法实现过期删除,那累积下去岂不是会超出内存吗?答案是:走内存淘汰机制。Redis 内存淘汰机制有以下几个:
- volatile-ttl:在设置了过期时间的数据集里,淘汰离过期时间最近的 key;
- volatile-random:在设置了过期时间的数据集里,淘汰任意一个 key;
- volatile-lru:在设置了过期时间的数据集里,淘汰最近最不常使用的 key;
- allkeys-random:在所有数据集里,淘汰任意一个 key,不符合常用的理论,所以一般不用;
- allkeys-lru:在所有数据集里,淘汰最近最不常使用的 key,最常用的机制;
- noeviction:当内存不足时,新写入操作会报错,一般没人用这种。
如果没有设置 expire 的key,不满足先决条件(prerequisites),那么volatile-lru, volatile-random 和 volatile-ttl 策略的行为,和 noeviction(不删除) 基本上一致。
具体讲解请转到博客:Redis 3.0 的六种缓存淘汰策略
这个也是线上非常常见的一个问题,简单的讲:就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
不要担心,Redis 自己就有天然解决这个问题的 CAS 类的乐观锁方案,使用版本号进行控制。
watch 指令在 Redis 事物中提供了 CAS 的行为, Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。注意使用 multi 开始事务,exec 提交事务。
Redis6.0之前,Redis在处理客户端的请求时,包括读socket、解析、执行、写socket等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
Redis6.0之后,采用“IO 多路复用”机制的线程模型,可以同时监听多个 Socket。但是,Redis 并非是完全摒弃单线程,还是使用单线程模型来处理客户端的请求,执行客户端命令,只是使用多线程来处理数据的读写和协议解析。
这样做的目的是因为 Redis 的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。
有些读者可能还会有疑问:哈希冲突链上的元素只能通过指针逐一查找再操作。当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就会降低了。
为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。
Redis一般都是集群部署的,假设数据在主从同步过程,主节点挂了,Redis分布式锁可能会有哪些问题呢?一起来看些这个流程图:
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock,Redlock核心思想是这样的:
搞多个 Redis master 部署,以保证它们不会同时宕掉。并且这些 master 节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个 master 实例上,是与在Redis 单实例,使用相同方法来获取和释放锁。
假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。请求来的时候,顺序向5个master节点请求加锁,根据设置的超时时间来判断,是不是要跳过该master节点,如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦,如果获取锁失败,则解锁。
简单理解:半数以上枷锁成功才算成功。
具体讲解请转到博客:Redis 与 DB 的数据一致 / 双写一致性问题
在项目中使用 Redis,肯定不会是单点部署 Redis 服务的。因为,单点部署一旦宕机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。
Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
主从模式
主从模式中,Redis 部署了多台机器,有主节点,负责读写操作,有从节点,只负责读操作。从节点的数据来自主节点,实现原理就是主从复制机制
主从复制包括全量复制,增量复制两种。一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制。slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发增量复制。
当master节点发生数据增减时,就会触发replicationFeedSalves()
函数,接下来在 Master节点上调用的每一个命令会使用replicationFeedSlaves()
来同步到Slave节点。执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。
哨兵模式
主从模式中,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址。显然,多数业务场景都不能接受这种故障处理方式。Redis从2.8开始正式提供了Redis Sentinel(哨兵)架构来解决这个问题。
哨兵模式,由一个或多个Sentinel实例组成的Sentinel系统,它可以监视所有的Redis主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点。但是呢,一个哨兵进程对Redis节点进行监控,就可能会出现问题(单点问题),因此,可以使用多个哨兵来进行监控Redis节点,并且各个哨兵之间还会进行监控。
简单来说,哨兵模式就三个作用:
- 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
- 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
- 哨兵之间还会相互监控,从而达到高可用。
集群模式
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,Cluster集群应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Redis节点上存储不同的内容,来解决在线扩容的问题。并且,它也提供复制和故障转移的功能。
一个Redis集群由多个节点组成,各个节点之间是怎么通信的呢?通过Gossip协议!
Redis Cluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot信息等等。常用的Gossip消息分为4种,分别是:ping、pong、meet、fail。
- meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
- ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。
- pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
- fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
每个节点是通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加1w。例如:如果某个node的端口号是6379,那么它与其它nodes通信的端口号是 16379。
nodes 之间的通信采用特殊的二进制协议。
面试官心理:
看看你了解不了解你们公司的 Redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 Redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 Redis 给几个 G 的内存?设置了哪些参数?压测后你们 Redis 集群承载多少QPS?
兄弟,这些你必须是门儿清的,否则你确实是没好好思考过。
面试题剖析:
可以这样说:Redis Cluster,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例, 每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到5 万/秒,5 台机器最多是 25 万读写请求/秒,完全满足我们的业务需求。
8 核 CPU + 32G 内存 + 1T 磁盘,但是分配给 Redis 进程的是 10G 内存,一般线上生产环境 Redis 的内存尽量不要超过 10G ,超过 10G 可能会有问题。5 台部署主实例的机器对外提供读写,一共有 50G 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
主要是商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,不到总内存的 50%,目前高峰期每秒就是 3500 左右的请求量。
如果用 Redis 缓存技术的话,肯定要考虑如何增加多台 Redis 服务器,保证 Redis 是高并发的,还有就是如何让 Redis 保证自己挂掉一台服务以后不会直接死掉,即 Redis 高可用。
这部分内容较多,概括来讲就是:
- Redis Sentinal(哨兵模式):基于哨兵实现高可用,Redis 实现高并发主要依靠主从架构,一主多从。一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。
- 如果想要在实现高并发的同时,容纳大量的数据,那么就需要 Redis Cluster 集群,使用 Redis 集群之后,可以提供每秒几十万的读写并发。
Redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,可以实现:任何一个实例宕机,可以进行主备切换。
Redis 在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的数据。RedisCluster 是 Redis 的亲儿子,它是 Redis 作者自己提供的 Redis 集群化方案。
Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分得更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,它不需要另外的分布式存储来存储节点槽位信息。
Redis Cluster 是一种服务器 Sharding 技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个 Redis 主机和多个从机组成,片区和片区之间是相互平行的。
Redis Cluster集群采用了P2P的模式,完全的去中心化。
Redis cluster 介绍:
在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如:一个是 6379,另外一个就是加1w 的端口号 16379。
6379 端口号做外部通信,而 16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西。cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, gossip
协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
集群元数据的维护有两种方式:集中式、Gossip 协议。Redis cluster 节点间采用 gossip 协议进行通信。
集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护,典型代表就是大数据领域的 Storm。Storm 是分布式的大数据实时计算引擎,是集中式的元数据存储的结构。
Gossip 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
集中式的好处:元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;不好在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
gossip 的好处:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- Redis cluster 的 hash slot 算法
Redis cluster 的高可用的原理,几乎跟哨兵是类似的。
如果一个节点认为另外一个节点宕机,那么就是
pfail
,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是fail
,客观宕机,跟哨兵的原理几乎一样,sdown,odown。在
cluster-node-timeout
内,某个节点一直没有返回pong
,那么就被认为pfail
。如果一个节点认为某个节点
pfail
了,那么会在gossip ping
消息中,ping
给其他节点,如果超过半数的节点都认为pfail
了,那么就会变成fail
。
从节点过滤
对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
检查每个 slave node 与 master node 断开连接的时间,如果超过了
cluster-node-timeout * cluster-slave-validity-factor
,那么就没有资格切换成master
。
从节点选举
每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node
(N/2 + 1)
都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。从节点执行主备切换,从节点切换为主节点。
与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,Redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
Redis cluster 最大的区别,就是数据分片,即:每个 master 都会持有部分数据,扩容起来非常方便。
①. 缓存雪崩
雪崩通常指大量缓存的 key 同一时间失效,该情况一般发生在缓存机器意外发生了全盘宕机时。这会导致某个服务的高并发请求,本该是从缓存中获取数据,但现在只能全部落到数据库,而关系型数据库必然扛不住如此之大的并发,它会报一下警,然后就挂了,最终导致依赖这个数据库的全部服务都不可用。即使 DBA 重启数据库也会立刻再次被打死,后果非常严重。
解决方案如下:
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
②. 缓存穿透
缓存穿透,指的是缓存和数据库都不存在的数据,通常来自于恶意攻击。这样的数据,缓存中不会有,所以请求每次都“绕过缓存”,直接查询数据库,数据库查不到不会缓存,导致以后请求都会落在数据库上。这种恶意攻击场景的缓存穿透,会直接把数据库给打死。
解决方案如下:
- 简单方案:对于没有从数据库查到的 key,就写一个空值到缓存里去,然后设置一个过期时间,这种方式虽然是简单,但并不优雅,在某些场景下会缓存过多的空值;
- 优雅方案:使用 bitmap 布隆过滤。
③. 缓存击穿
缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
不同于缓存穿透, 缓存击穿指的是缓存没数据,但是数据库有数据的情况。
不同场景下的解决方式可如下:
- 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期;
- 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存;
- 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
不管是恶意流量攻击引发的缓存击穿还是穿透,其实还有一个好办法可以防范:Redis 布隆过滤器 - RedisBloom 。
以用户表数据为例,使用布隆过滤器,把所有的 user 表里的关键查询字段放于 Redis 的 bloom 过滤器内。有人会说,这不疯了,我有4000万会员?so what!
布隆过滤器内不是直接把key - value这样放进去的,它存放的内容是这么一个 bitmap中,bitmap非常节省空间。
布隆过滤器可以用于检索一个元素是否在一个集合中。它可以告诉你某种东西一定不存在或者可能存在。当布隆过滤器说,某种东西存在时,这种东西可能不存在;当布隆过滤器说,某种东西不存在时,那么这种东西一定不存在。
Redis4.0 版本提供了该功能,在该功能的基础上才有了布隆过滤器 - RedisBloom 。
Bit-map就是用一个bit位来标记某个元素对应的Value,通过Bit为单位来存储数据,可以大大节省存储空间。所以我们可以通过一个int型的整数的32比特位来存储32个10进制的数字,那么这样所带来的好处是内存占用少、效率很高(不需要比较和位移)。
使用 Redis 布隆过滤器 - RedisBloom 需要先给 Redis 安装 Bloom Filter。安装教程省略...
1. 有10亿个号码,现在又来了10万个号码,如何快速准确判断这10万个号码是否在10亿个号码库中?
2. 虫的网站千千万万,对于一个新的网站url,我们如何判断这个url我们是否已经爬过了?
3. 垃圾邮箱的过滤?
对于类似这种,大数据量集合,如何准确快速的判断某个数据是否在大数据量集合中,并且不占用内存,都可以使用 Redis 布隆过滤器。
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
Redis cluster 有固定的 16384
个 hash slot,对每个 key
计算 CRC16
值,然后对 16384
取模,可以获取 key 对应的 hash slot。
Redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag
来实现。
最大的好处在于:任何一台机器宕机,其他的节点不受影响的,因为 key 找的是 hash slot,不是机器。
①. 基于内存的存储实现
内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。
②. 高效的数据结构
Mysql 索引为了提高效率,选择了B+树的数据结构。可见,合理的数据结构就是能够让你的应用/程序更快。先看下 Redis 的数据结构&内部编码图:
重点说一下字符串结构类型:SDS简单动态字符串。
- 字符串长度处理:Redis 获取字符串长度,时间复杂度为O(1),而C语言中,需要从头开始遍历,复杂度为O(n);
- 空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
- 惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配。
- 二进制安全:Redis可以存储一些二进制数据,在C语言中字符串遇到'\0'会结束,而 SDS中标志字符串结束的是len属性。
具体讲解请转到博客:Redis为什么快?基于内存就完事了?无视Redis如此完美的数据结构...
③. 合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候使用什么样数据结构,使用什么样编码,是 Redis 设计者总结优化的结果。
- String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。
- List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码
- Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。
- Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。
- Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码
④. 合理的线程模型
Redis 采用 IO 多路复用机制,同时监听多个 socket。多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
⑤. 虚拟的内存机制
虚拟内存机制:就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis 直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
持续更新,感谢支持...