【Java面试不跪】高并发系列之Redis拷打
写在前面
作为后端/服务端/大数据场景下的常客,redis的应用范围及其广泛,作为key/value数据库,在系统架构中一般承担着非常非常非常重要的作用,掌握并能够将redis应用在实际项目中,甚至可以去解决相关的问题,是作为一名成熟的RD所必须的能力!
拷打场景
- 为什么要使用redis作为缓存?
- redis如何实现数据不丢失?
- redis如何实现服务高可用?
- redis集群脑裂导致数据丢失怎么办?
- redis内存满了怎么办?
- 如何避免缓存击穿?
- 如何设计一个缓存策略,以此来动态缓存热点数据?
- 如何保证缓存和数据库数据的一致性?
- redis的大key/热key问题怎么办?
文章目录
1 redis 基本介绍
2 redis 线程模型
3 redis持久化
4 redis 集群
5 redis过期删除与内存淘汰
6 redis实战:缓存设计应用?
1 redis 基本介绍
Redis 是一种基于内存的KV数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
为什么要使用redis作为缓存?
主要是因为 Redis 具备「高性能」和「高并发」两种特性。
1)高性能
假如用户第一次访问 MySQL 中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。如果将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。
2)高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
2 redis线程模型
Redis 4.0之前一直采用单线程。
Redis单线程是Redis非常重要的区别于其他数据的一个非常重要的特性,也是它有如此高性能的一个主要的影响因素。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
redis单线程模式
为什么Redis使用单线程,性能却很好呢?
官方解释:因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
但是官方的解释还是有点难懂了,我们用比较通俗的语言来解释下。
首先,Redis的整个处理流程是什么样的?
说白了,以读取数据为例,比如一个读请求,最基本的流程就是:「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」,Redis其实就是将整个核心流程放在主线程中运行,那么Redis做的优化就是把网络操作和IO操作分离出来单独进行优化。
核心就是使用IO多路复用。
IO多路复用:一个线程发起并且同时监听多个IO通道的状态,就叫做 IO多路复用。
核心就是:利用一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
3 redis持久化
redis如何实现数据不丢失?
redis的数据全部在内存中,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证redis的数据在发生突发状况时不会丢失、或者只丢失少量,于是必须根据一些策略来把redis内存中的数据写到磁盘中,这样当redis服务重启时,就会将硬盘中的数据恢复到内存中。Redis持久化的意义就是为了保证突然宕机,内存数据不会全部丢失。
redis有两种持久化机制:RDB和AOF。
Redis4.0后支持RDB和AOF两种持久化机制混合使用,所以存在三种持久化策略。
RDB是基于快照一次的全量备份,即周期性的把redis当前内存中的全量数据写入到一个快照文件中(周期时间可以通过配置来调整)。redis是单线程程序,这个线程要同时负责多个客户端的读写请求,还要负责周期性的把当前内存中的数据写到快照文件中RDB中,数据写到RDB文件是IO操作,IO操作会严重影响redis的性能,甚至在持久化的过程中,读写请求会阻塞,为了解决这些问题,Redis采用多进程来同时进行读写请求和持久化操作。这样又会导致另外的问题,持久化的过程中,内存中的数据还在改变,假如redis正在进行持久化一个大的数据结构,在这个过程中客户端发送一个删除请求,把这个大的数据结构删掉了,这时候持久化的动作还没有完成,那么redis该怎么办呢?
redis使用操作系统的多进程COW(Copy On Write)机制来实现快照的持久化,在持久化过程中调用 glibc(Linux下的C函数库) 的函数fork()产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端的读写请求。子进程刚刚产生时,和父进程共享内存里面的代码段和数据段,这是Linux操作系统的机制,为了节约内存资源,所以尽可能让父子进程共享内存,这样在进程分离的一瞬间,内存的增长几乎没有明显变化。
子进程对当前内存中的数据进行持久化时,并不会修改当前的数据结构,如果父进程收到了读写请求,那么会把处理的那一部分数据复制一份到内存,对复制后的数据进行修改。所以即使对某个数据进行了修改,redis持久化到RDB中的数据也是未修改的数据,这也是把RDB文件称为"快照"文件的原因,子进程所看到的数据在它被创建的一瞬间就固定下来了,父进程修改的某个数据只是该数据的复制品。
AOF(Append-only file)日志存储的是redis服务器的顺序指令序列,即对内存中数据进行修改的指令记录。当redis收到客户端修改指令后,先进行参数校验,如果校验通过,先把该指令存储到AOF日志文件中,也就是先存到磁盘,然后再执行该修改指令。
redis把操作指令追加到AOF文件这个过程,并不是直接写到AOF文件中,而是先写到操作系统的内存缓存中,这个内存缓存是由操作系统内核分配的,然后操作系统内核会异步地把内存缓存中的redis操作指令刷写到AOF文件中。当redis宕机后重启后,可以读取该AOF文件中的指令,进行数据恢复,恢复的过程就是把记录的指令再顺序执行一次,这样就可以恢复到宕机之前的状态。
4 redis集群
Redis集群是一种通过将多个Redis节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许Redis在不同节点上同时提供服务,提高整体性能和可靠性。
redis如何实现服务高可以用?
集群模式主要为三种类型:
1)主从复制是Redis的一种基本集群模式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点(从节点)来实现数据的冗余和备份。主节点负责处理客户端的写操作,同时从节点会实时同步主节点的数据。客户端可以从从节点读取数据,实现读写分离,提高系统性能。
优点:
- 配置简单,易于实现。
- 实现数据冗余,提高数据可靠性。
- 读写分离,提高系统性能。
缺点:
- 主节点故障时,需要手动切换到从节点,故障恢复时间较长。
- 主节点承担所有写操作,可能成为性能瓶颈。
- 无法实现数据分片,受单节点内存限制。
2)哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。哨兵节点是一种特殊的Redis节点,它会监控主节点和从节点的运行状态。当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移。
优点:
- 自动故障转移,提高系统的高可用性。
- 具有主从复制模式的所有优点,如数据冗余和读写分离。
缺点:
- 配置和管理相对复杂。
- 依然无法实现数据分片,受单节点内存限制。
3)Cluster模式是Redis的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。在Cluster模式下,Redis将所有的键值对数据分散在多个节点上。每个节点负责一部分数据,称为槽位。通过对数据的分片,Cluster模式可以突破单节点的内存限制,实现更大规模的数据存储。
优点:
- 数据分片,实现大规模数据存储。
- 负载均衡,提高系统性能。
- 自动故障转移,提高高可用性。
缺点:
- 配置和管理较复杂。
- 一些复杂的多键操作可能受到限制。
redis集群脑裂导致数据丢失怎么办?
所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
为什么会发生脑裂?
1.确认是不是数据同步出现了问题
在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。
如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断,也就是计算 master_repl_offset 和 slave_repl_offset 的差值。如果从库上的 slave_repl_offset 小于原主库的 master_repl_offset,那么,我们就可以认定数据丢失是由数据同步未完成导致的。
2.排查客户端的操作日志,发现脑裂现象
在排查客户端的操作日志时,我们发现,在主从切换后的一段时间内,有一个客户端仍然在和原主库通信,并没有和升级的新主库进行交互。这就相当于主从集群中同时有了两个主库。根据这个迹象,我们就想到了在分布式主从集群发生故障时会出现的一个问题:脑裂。
但是,不同客户端给两个主库发送数据写操作,按道理来说,只会导致新数据会分布在不同的主库上,并不会造成数据丢失。那么,为什么我们的数据仍然丢失了呢?
为什么脑裂会导致数据丢失?
主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of 命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
如何应对脑裂问题?
Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。
min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量; min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。 我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。
5 redis过期删除与内存淘汰
redis 内存满了怎么办?
核心是两种方法:
第一种:直接给redis扩容,让它有足够的容量
第二种:如果没有足够的资源扩容,可以尝试使用淘汰策略,定期清理一些不常用的数据
常见的内存淘汰策略为:
实际上Redis定义了 「 8种内存淘汰策略 」 用来处理redis内存满的情况:
noeviction:直接返回错误,不淘汰任何已经存在的redis键
allkeys-lru:所有的键使用lru算法进行淘汰
volatile-lru:有过期时间的使用lru算法进行淘汰
allkeys-random:随机删除redis键
volatile-random:随机删除有过期时间的redis键
volatile-ttl:删除快过期的redis键
volatile-lfu:根据lfu算法从有过期时间的键删除
allkeys-lfu:根据lfu算法从所有键删除
6 redis实战:缓存设计应用?
如何避免缓存击穿?
什么是缓存击穿:经常被查询的一个Key突然失效或者宕机了,导致重建缓存,由于是热点Key,所以有不断的线程来查和重建缓存,导致大量数据到达数据库,这种我们称为缓存击穿。
其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。
分析:
关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。
解决方案:
1、如果业务允许的话,对于热点的key可以设置永不过期的key。
2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。
如何设计一个缓存策略,以此来动态缓存热点数据?
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
以购物场景中购买商品的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:
- 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
- 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
- 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。
如何保证缓存和数据库数据的一致性?
如果用到了缓存,就会涉及到缓存与数据库双存储双写,只要是双写,就一定会有数据一致性的问题,那么如何解决一致性问题?
既然是对缓存和数据库都进行操作,就包括了两个步骤,那么存在第一步操作成功,第二步操作失败问题。
1.Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。步骤如下:
写:先更新 DB。然后删除缓存。
读:从 cache 中读取数据,读取到就直接返回;cache中读取不到的话,就从 DB 中读取数据返回,再把数据放到 cache 中。
2.Read/Write Through Pattern(读写穿透)
读写穿透中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。 Redis 并没有提供 cache 将数据写入DB的功能,该模式用的较少。
写:先查 cache,cache 中不存在,直接更新 DB。cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB),对客户端透明。
读:从 cache 中读取数据,读取到就直接返回 。读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
3.Write Behind Pattern(异步缓存写入)
异步缓存写入和读写穿透很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
但是,两个又有很大的不同:读写穿透是同步更新 cache 和 DB,而异步缓存写入则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
所以如果缓存挂掉而数据没有更新的话,就会造成数据丢失,适用于对数据一致性要求没那么高的场景。
redis的大key/热key问题怎么办?
在工作中,Redis作为一款高性能缓存数据库被广泛应用,但常遇到“大key”和“热key”问题。“大key”指单个键包含大量数据,导致内存消耗高、性能下降及持久化效率降低;“热key”则是频繁访问的键,会引起CPU占用率高、请求阻塞等问题。
大key影响:
内存消耗: 在进行缓存时降低缓存的效率,占用大量的内存空间,使得 Redis 的内存消耗急剧增加,还可能导致 Redis 实例的内存资源不足,甚至出发内存淘汰策略,从而影响系统的正常运行。
性能下降:处理大的 key,会耗费更多的 CPU 时间以及带宽,导致 Redis 性能下降。由于 Redis 还是单线程的,处理 大key
的操作进而会阻塞其他请求的处理,从而影响系统性能。
持久化效率降低: 在进行持久化操作时,AOF
与RDB
都会因为该 大key
耗费更多的时间,从而延迟持久化时间,分布式环境下甚至会造成缓存不一致。
网络传输延迟: 大key
在进行网络传输时会增加网络传输的延迟,在分布式环境下进行数据同步时可能会造成数据的不一致。
热key影响:
CPU占用率高: 因为是 热key
,所以 CPU 一直占用,进而导致Redis实例的CPU负载增加。
请求阻塞:如果 key 有访问优先级,热key
的存在可能导致请求队列中其他的请求被阻塞。
响应时间延长:因为 热key
,其他的请求被阻塞了造成响应时间延长。
性能不均衡:流量访问造成突刺,系统性能的不均衡。
大key的解决方案
• 合理的数据结构:减少value大小
• 合理的缓存时间:设置过期时间
• 大key 进行拆分为多个小key
• 定期对 大key 进行清理
热key的解决方案
• 合理的缓存淘汰策略:需要利用策略淘汰
• 热点数据分片:将热点数据分散到不同的Redis实例,提升系统的吞吐量。
• 缓存预热:在系统启动或者活动高峰开启之前进行缓存预热,提前将需要的数据加载到缓存,减少热点数据首次访问的时间。
• 随机缓存失效时间:避免大量的key同一时间批量失效,造成缓存雪崩与缓存穿透。
• 缓存穿透:使用布隆过滤器进行缓存请求过滤,防止无效请求进入到缓存层。
#后端##校招##java##redis#针对实习秋招的同学,无论你是零基础入门还是已经在刷题的道路上驰骋的同学。在这里,你都能针对性的提高自己的刷题能力,提升自己对算法题的认知。 本专栏目的在于帮助需要帮助的同学顺利拿到实习以及秋招的offer!