1.Redis的产生
只要是上网的APP基本上都需要和相应的服务器请求数据,通常来说,这些数据被服务器保存在“磁盘”上的文件中,称之为**“磁盘型数据库”。但是面对海量用户时(比如秒杀活动),磁盘IO的读写速率不够快从而导致用户体验下降,并且服务器数据库的压力也非常大。鉴于很多请求只是读取数据,这就启发我们将一些热点数据存放在内存中,以便快速响应请求、并且减轻磁盘的读写压力,即引入Redis** 。
2.Redis相关名词解释
2.1 缓存穿透
-
定义:缓存穿透指的是请求绕过缓存,直接访问后端数据库。这通常发生在缓存中没有找到对应的数据,而请求直接穿透到数据库层,导致数据库压力增大。
-
原因:可能是由于请求中的数据不存在,或者请求的数据从未被缓存过。
-
解决方案:可以使用布隆过滤器(Bloom Filter)来拦截无效请求,避免这些请求直接到达数据库。
布隆过滤器(Bloom Filter):在系统启动或者定期更新时,将数据库中有效的请求数据(例如,有效的用户名、有效的查询条件等)的哈希值加入到布隆过滤器中。如果布隆过滤器确定请求中的数据不在集合中,那么可以认为这是一个无效请求,系统可以直接拒绝这个请求,而不需要进一步查询数据库。使用布隆过滤器的优点包括:
-
速度快:布隆过滤器的检查操作非常快速,因为它只涉及哈希计算。
-
节省空间:相比于存储所有有效数据,布隆过滤器只需要很少的内存空间。
缺点包括:
-
误报:可能会错误地拒绝一些实际上有效的请求,但这种情况相对较少。
-
不可删除:一旦数据被加入到布隆过滤器中,就无法从过滤器中删除,这可能会导致随着时间的增长,误报率上升。
-
2.2 缓存击穿
-
定义:缓存击穿发生在缓存中的某个热点数据的缓存过期或被清除时,导致大量并发请求同时访问后端数据库,造成数据库负载突然增加。
-
原因:缓存中的数据过期或者被清除,而这些数据正好是高频访问的热点数据。
-
解决方案:可以使用互斥锁(Mutex)或加锁机制,确保只有一个请求能够从数据库中加载数据,并将数据写入缓存。
2.3 缓存雪崩
-
定义:缓存雪崩指的是大量缓存数据在同一时间点过期或失效,导致大量请求同时访问后端数据库,从而导致数据库压力剧增,甚至崩溃。
-
原因:缓存中的大量数据在相同时间过期。
-
解决方案:可以采用不同的缓存过期时间来避免数据集中失效,使用预热缓存策略来提前加载数据,或者设置合理的缓存过期策略和备份机制。
设置合理的缓存过期策略和备份机制:
1. 定时删除
-
定义:定时删除是一种缓存过期策略,按照设定的时间间隔定期检查并删除过期的缓存数据。
-
实现方式:Redis 内部定期扫描和删除过期键(如使用定时器)。
-
优缺点:优点是简单易懂;缺点是可能导致不必要的性能开销,因为 Redis 需要周期性地扫描过期键。
2. 惰性删除
-
定义:惰性删除是指在访问缓存数据时,如果发现数据已经过期,才会删除该数据并重新加载。
-
实现方式:当数据过期时,Redis 只在实际访问时删除过期数据,而不是定期扫描。
-
优缺点:优点是减少了删除操作的频率;缺点是可能会造成请求延迟,因为需要在访问时才进行删除操作。
3. 内存淘汰
-
定义:内存淘汰指的是在 Redis 内存使用达到限制时,Redis 需要选择性地删除一些缓存数据以释放内存空间。
-
策略:
-
LRU(Least Recently Used):删除最近最少使用的数据。
-
LFU(Least Frequently Used):删除使用频率最低的数据。
-
随机淘汰:随机删除一些数据。
-
优先过期:优先删除过期的数据。
-
4. RDB + AOF
-
定义:RDB(Redis 数据库备份)和 AOF(Append Only File)是 Redis 的两种持久化机制。
-
RDB:定期将数据快照保存到磁盘上,以备份数据。
-
AOF:记录所有写操作日志,并以追加方式保存到文件中,可以更细粒度地恢复数据。
-
-
组合使用:RDB 提供快速恢复,AOF 提供更高的数据持久性保障,组合使用可以提供更好的数据安全性和恢复能力。
-
2.4 主观下线、客观下线
-
主观下线:
-
定义:在 Redis 集群中,主观下线是指当 Redis 实例被认为是不可用(如通过哨兵或集群管理工具标记为下线),但实际上它可能仍然在运行。
-
原因:通常是由于网络问题或与主节点的通信失败导致。
-
-
客观下线:
-
定义:客观下线是指 Redis 实例实际上已经崩溃或关闭,并且无法提供服务。
-
确认:通过检查节点的实际状态(如心跳检测、服务不可用等)。
-
2.5 哨兵选举
-
定义:Redis 哨兵(Sentinel)用于监控 Redis 主从复制集群的健康状态,并在主节点发生故障时自动进行主节点选举。
-
过程:哨兵节点通过不断检查主节点和从节点的健康状态,选举一个新的主节点来接管故障主节点的工作。
2.6 故障转移
-
定义:故障转移是指当 Redis 主节点发生故障时,系统自动将一个从节点提升为新的主节点,以保持系统的可用性。
-
过程:故障转移通常由 Redis 哨兵完成,它监控主节点的状态,发现故障后,选择一个从节点成为新的主节点,并将其他从节点同步到新的主节点上。
2.7 集群工作 + 主从复制
-
集群工作:
-
定义:Redis 集群是一种分布式解决方案,将数据分布在多个节点上(利用哈希筒实现),实现数据的分片和高可用性。
-
优点:支持水平扩展,提高了系统的吞吐量和容错能力。
-
-
主从复制:
-
定义:Redis 主从复制是一种数据备份机制,其中一个主节点(Master)将数据复制到多个从节点(Slave),实现数据的冗余和备份。
-
优点:提高了数据的可用性和系统的读操作性能。
-
3.一些典型的面试题
为什么要使用Redis?
从高并发上来说:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去。这样用户的一部分请求会直接到缓存,而不用经过数据库。
从高性能上来说:用户第一次访问数据库中的某些数据,因为是从硬盘上读取的,所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
为什么要使用Redis而不是其他的,例如Java自带的map或者guava?
缓存分为本地缓存和分布式缓存,像map或者guava就是本地缓存。
本地缓存最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束。在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。(无分布式锁机制)
redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。 (有分布式锁机制)
Redis应用场景有哪些?
缓存热点数据,缓解数据库的压力
利用Redis原子性的自增操作,可以实现计数器的功能。比如统计用户点赞数、用户访问数等。
分布式锁。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用Redis自带的SETNX命令实现分布式锁,除此之外,还可以使用官方提供的RedLock分布式锁实现。
RedLock 算法的工作原理
RedLock 算法是为了解决在分布式环境下安全可靠地获取分布式锁的问题。该算法涉及在多个 Redis 实例上同时设置锁,以确保锁的持有是分布式环境中全局唯一的。以下是详细解释:
多个 Redis 实例:RedLock 要求在多个独立的 Redis 实例上(例如 5 个)设置锁。这些实例可以部署在不同的物理或虚拟服务器上,保证高可用性。
锁的获取:
客户端依次尝试在所有 Redis 实例上设置同一个锁,每个实例都使用相同的锁键和锁值(例如唯一标识符,如 UUID)。
为了成功获取锁,客户端必须在大多数(N/2 + 1)个 Redis 实例上成功设置锁。例如,如果有 5 个 Redis 实例,客户端需要在至少 3 个实例上成功设置锁。
为什么需要多个实例?
防止单点故障:通过在多个实例上设置锁,RedLock 能够防止单点故障。如果只有一个 Redis 实例,锁的安全性完全依赖于这个实例。如果它发生故障,整个锁机制就会失效。
提高容错性:多个实例可以保证即使部分实例发生故障(例如 1 或 2 个实例不可用),锁的机制仍然能够可靠工作。如果客户端在大多数实例上成功设置了锁,即便有少数实例失败,它仍然可以安全地认为自己持有了锁。
避免脑裂:在分布式系统中,如果网络分区或其他原因导致不同的客户端各自认为自己持有锁,就可能发生“脑裂”。通过要求在多个实例上成功设置锁,RedLock 减少了这种情况发生的可能性。
锁的唯一性:
锁虽然在多个实例上设置,但它们的设置是原子的(通过
SETNX
实现),并且在所有实例中锁的键和值是相同的。因为每个实例上设置的锁都是唯一的标识符,所以即使多个实例上同时设置了锁,这些锁也指向同一个资源,并且只有第一个成功的设置操作会被认为是成功获取锁。异步操作。可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。
限速器。可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。
实现速率限制
以下是两种常见的速率限制实现方式:
1. 固定窗口计数器
这种方法基于固定时间窗口(如每秒、每分钟)来计数请求。
实现步骤:
为每个用户创建一个唯一的计数器键,例如
rate_limit:user_id:timestamp
,其中timestamp
表示当前时间的固定时间窗口(如秒)。使用 Redis 的
INCR
命令增加计数器的值。设置计数器的过期时间,确保窗口结束后计数器被自动清除。
如果计数器的值超过限制,则拒绝请求。
2. 滑动窗口计数器
滑动窗口计数器提供了更平滑的速率限制,避免了固定窗口方法中的突发情况。
实现步骤:
使用 Redis 的 List 数据结构来记录每个用户的请求时间戳。
每次请求时,将当前时间戳加入 List。
使用
LRANGE
命令检查 List 中的时间戳,计算请求的数量。删除超出时间窗口的时间戳。
如果请求数量超过限制,则拒绝请求。
应用场景:秒杀
在秒杀活动中,通常会有大量用户在极短的时间内尝试访问某个接口或资源。速率限制可以有效防止:
恶意刷票:用户通过快速重复点击来获取更多资源或优惠,导致系统负载过高。
资源过载:高并发请求可能会超出系统的处理能力,导致服务不可用。
公平性问题:通过限制每个用户的请求频率,确保所有用户都有公平的机会参与秒杀活动。
为什么Redis这么快?
Redis是基于内存进行数据操作的Redis使用内存存储,没有磁盘IO上的开销,数据存在内存中,读写速度快。
采用IO多路复用技术。Redis使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。
1. Redis 使用单线程
单线程模型:
Redis 的核心是基于单线程的,即它在一个主线程上处理所有的客户端请求,而不是使用多个线程并发处理。
这种设计意味着 Redis 不需要考虑多线程同步问题,从而避免了锁竞争等复杂性,简化了开发,并且提升了性能。
2. 轮询描述符
文件描述符:
文件描述符(File Descriptor,FD)是操作系统用来标识打开的文件、网络连接等资源的整数编号。
在网络编程中,每个客户端连接都对应一个文件描述符。
轮询描述符:
Redis 使用事件驱动的模型(基于
epoll
、select
、kqueue
等系统调用),它会轮询这些描述符,检查哪些描述符有事件需要处理(如网络数据到达、客户端连接关闭等)。这种轮询机制使得 Redis 能够高效地处理大量的客户端连接,即使是单线程,也能在短时间内响应多个连接请求。
3. 事件驱动模型
事件驱动:
Redis 将所有操作都视作事件来处理,如读写数据、管理连接、执行命令等。这些事件被放入事件循环中,按顺序处理。
事件驱动模型通过将 I/O 操作(如网络通信、文件读写)与业务逻辑分离,避免了阻塞操作,保证了 Redis 的高效性。
不在网络 I/O 上浪费过多的时间:
由于 Redis 使用非阻塞 I/O,结合事件驱动机制,能够在轮询到有数据可读/可写的描述符时,立即处理相应的操作,而不是等待数据的到来。这减少了 I/O 等待时间,提高了系统响应速度。
高效的数据结构。Redis每种数据类型底层都做了优化,目的就是为了追求更快的速度。
高效数据结构:
Redis 提供了一些经过高度优化的数据结构,如字符串、列表、集合、有序集合、哈希表等。这些数据结构在内存中采用紧凑、高效的方式存储和操作,确保了高性能的读写速度。
例如:
字符串(String):用于存储简单的键值对,支持各种操作如拼接、截取等。
列表(List):双向链表,支持快速的头尾插入和删除操作。
集合(Set):无序集合,支持快速的成员添加、删除、检查操作。
有序集合(Sorted Set):带有分数的集合,支持按分数排序,快速的范围查询等。
哈希表(Hash):用于存储多个键值对的集合,支持快速的字段操作。
补充:
什么是“脑裂”?
“脑裂”指的是在分布式系统中,由于网络分区或其他故障,系统的多个部分无法相互通信,从而导致每个部分各自独立运行,并且都认为自己是系统的唯一或主要部分。具体到分布式锁的情境中,“脑裂”会导致多个客户端同时认为自己成功获得了锁,尽管实际上这种锁应该是独占的。
举例说明“脑裂”问题
假设一个分布式系统中有两个客户端(A 和 B)和三个 Redis 实例(X、Y 和 Z),它们通过 RedLock 算法来获取一个分布式锁。
-
正常情况:
-
客户端 A 通过 RedLock 成功在 X、Y、Z 这三个实例上获取了锁。
-
在正常情况下,A 持有锁,其他客户端(如 B)在尝试获取锁时会发现锁已经被占用,只能等待。
-
-
网络分区发生:
-
假设网络发生故障,导致 Redis 实例 Z 与其他两个实例(X 和 Y)断开连接。这时系统产生了网络分区:
-
实例 X 和 Y 可以正常通信,并且客户端 A 认为自己仍然持有锁。
-
实例 Z 无法与 X 和 Y 通信,但可能仍然能够响应客户端 B 的请求。
-
-
-
“脑裂”现象:
-
如果客户端 B 现在试图获取锁,由于网络分区的存在,它可能只能与实例 Z 通信。如果 Z 没有意识到锁已经在 X 和 Y 上被获取,那么 B 可能会在 Z 上成功设置锁,并认为自己成功获取了锁。
-
结果是,客户端 A 和 B 都认为自己持有了同一把锁,尽管锁本应是独占的。
-
-
后果:
-
数据不一致:如果 A 和 B 是两个处理共享资源的客户端,它们同时认为自己持有锁并修改资源,这可能导致数据不一致或冲突。
-
系统异常:由于系统的不同部分“各自为政”,系统可能无法正确地协调和恢复,导致严重的操作错误或数据丢失。
-
如何防止“脑裂”?
为了防止和应对“脑裂”问题,分布式系统通常会采用一些策略:
-
多数派策略(Quorum):
-
RedLock 就是通过“多数派”策略来减少脑裂的可能性。通过在多个 Redis 实例上设置锁,并要求必须在大多数实例(如 5 个中的至少 3 个)上成功设置锁,来避免因少数实例故障而引发的脑裂。
-
-
心跳检测和故障检测:
-
使用心跳信号或故障检测机制来快速识别和处理网络分区或节点失效,确保系统能够及时恢复正常状态。
-
-
租约和超时机制:
-
分布式锁通常会设置一个超时时间(租约时间),当锁持有者在一段时间内未续约锁时,其他客户端可以尝试获取锁,这可以防止锁长期被一个不可用的客户端占用。
-
-
一致性协议:
-
使用一致性协议(如 Paxos 或 Raft)来确保在网络分区的情况下,系统依然能够保持数据一致性和操作的唯一性。
-