Skip to content

中间件(Redis 与消息队列) ​

配套知识点:Redis、消息队列、Kafka 深入

怎么用这份笔记

  • 分级:★★★ 必须吃透 / ★★☆ 了解即可(记结论就行) / ★☆☆ 可以跳过(一句话看一眼)
  • 【简历亮点,高频追问】:简历里写过的技术,面试官会顺着往下挖,这段必须能展开讲
  • 每个考点按「定义 → 原理 → 要点 / 易错点」组织,只讲知识点

1. Redis 为什么快(单线程模型)★★★ ​

四件事:纯内存操作、单线程避免锁竞争和线程切换、IO 多路复用(epoll 一个线程监听大量连接)、数据结构高效。

「单线程」指的是命令执行是单线程,Redis 6.0 之后网络 IO 的读写已经是多线程了。因此单个慢命令(keys *、删大 key)会卡住整个实例。

实践上要避开这类命令:遍历不要用 keys *,改用 scan 游标分批取;删除大 key 用 unlink 异步释放内存,避免同步 del 阻塞主线程。


补充(原文详解)

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问 1) 绝大部分请求是纯粹的内存操作(非常快速)2)采用单线程,避免了不必要的上下文切换和竞争条件 3)非阻塞IO优点:

速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

支持丰富数据类型,支持string,list,set,sorted set,hash

支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的

并发竞争key问题

同时有多个子系统去set一个key。这个时候要注意什么呢? 不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此, redis的事务机制,十分鸡肋。 (1) 如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可 (2) 如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设这会系统B先抢到锁,将key1设置为

{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做 set操作了。以此类推。 (3) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。

2. Redis 数据类型及底层结构 ★★★ ​

五种基础类型的典型用途:

  • String:字符串、计数,也用来做 Bitmap(位图,如在线 / 签到统计)。
  • Hash:存对象,一个 key 下挂多个字段,例如设备状态里的电量、功率、状态。
  • List:列表、队列。
  • Set:去重。
  • ZSet(有序集合):排行榜、延迟任务。

底层结构:

  • ZSet 用跳表,查询 O(logn)、范围查询友好,比红黑树好实现;数据量小时用 ziplist / listpack 省内存。
  • String 用 SDS,带长度字段,取长度为 O(1),并且是二进制安全的。
  • HyperLogLog 用于 UV 估算,固定的小内存换一定误差。

跳表的具体实现不用展开,记结论即可。


3. 缓存穿透、击穿、雪崩 ★★★ 【简历亮点,高频追问】 ​

三个问题现象相似,原因完全不同:

  • 穿透:查一个根本不存在的数据,缓存和库都没有,每次请求都打到库。解法:缓存空值(过期时间设短,通常不超过 5 分钟)、布隆过滤器、参数校验。布隆过滤器用多个独立哈希函数把数据映射到位数组,只会误判「存在」、不会漏判,所以能挡住一定不存在的数据。
  • 击穿:某个热点 key 刚好过期,瞬间大量请求一起打到库。解法:互斥锁重建(只放一个请求去查库回填)、热点 key 不设过期、逻辑过期。
  • 雪崩:大批 key 同时过期,或 Redis 整个挂了,请求全砸到库。解法:过期时间加随机值打散、多级缓存、集群高可用、限流降级。

补充(原文详解)

一、缓存雪崩

我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了, 而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 解决办法: 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开。

二、缓存穿透缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 解决办法; 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?

对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。 Bitmap:典型的就是哈希表缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。

布隆过滤器(推荐) 就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。 它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲

突”。 Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。 Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。

三、缓存预热缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理

解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! 解决思路: 1、直接写个缓存刷新页面,上线时手工操作下; 2、数据量不大,可以在项目启动的时候自动进行加载; 3、定时刷新缓存;

四、缓存更新除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: ( 1)定时去清理过期的缓存; ( 2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

五、缓存降级当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 以参考日志级别设置预案: ( 1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; ( 2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; ( 3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; ( 4)严重错误:比如因为特殊原因数据错误

了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

4. 缓存与数据库一致性 ★★★ 【简历亮点,高频追问】 ​

最主流的是 Cache Aside(旁路缓存):读时先查缓存,未命中再查库并回填缓存;写时先更新数据库,再删除缓存。

两个选择的理由:

  • 用删除而不是更新缓存:删除更简单,也能避免并发写把旧值刷进缓存。
  • 不能先删缓存再更新库:中间存在窗口,别的请求会在这个窗口读到旧数据并回填回缓存。

要做到最终一致,可以再加「延时双删」(更新库后延迟一段时间再删一次),或订阅 binlog(如 Canal)异步删缓存。缓存场景一般只承诺最终一致,做不到强一致;对一致性要求极高的数据(如支付金额)宁可直接读库不走缓存,缓存只用在允许短暂不一致的展示类数据上。


5. Redis 持久化:RDB 与 AOF ★★★ ​

  • RDB:定时把内存数据快照存盘,由 fork 子进程写,主进程照常服务。文件小、恢复快,但两次快照之间的数据会丢。
  • AOF:记录每条写命令追加到日志。刷盘频率有三档:everysec 是默认折中档,最多丢 1 秒数据;always 每条都刷盘,最安全也最慢;no 交给操作系统决定。文件比 RDB 大、恢复慢,靠 AOF 重写压缩体积。
  • 混合持久化(4.0+):RDB 快照 + 增量 AOF,恢复快又少丢数据,是更稳的默认选择。

纯缓存用途的实例可以只开 RDB;对丢数据更敏感的实例用混合持久化。


6. Redis 过期删除策略与内存淘汰 ★★★ ​

过期删除是两种策略结合,平衡 CPU 和内存:

  • 惰性删除:访问 key 时才判断是否过期,过期就删。
  • 定期删除:周期性随机抽一批 key 检查,删掉其中已过期的。

内存达到 maxmemory 后按 maxmemory-policy 淘汰:

  • noeviction:不淘汰,写命令直接报错(默认)。
  • allkeys-lru:在所有 key 里淘汰最久未使用的。
  • volatile-lru:只在设置了过期时间的 key 里按 LRU 淘汰。
  • allkeys-lfu:按访问频率淘汰。

注意:不设 maxmemory 是最危险的配置,Redis 会一直吃内存,直到被系统的 OOM Killer 杀掉,所以一定要显式设置上限和淘汰策略。


7. Redis 分布式锁(Redisson)★★★ 【简历亮点,高频追问】 ​

最基础的写法是 SET key value NX EX 秒:NX 保证 key 不存在时才设置,EX 加过期时间防止死锁。value 要放唯一标识(如 UUID + 线程 id),释放时先校验 value 是自己的再删,防止误删别人的锁。

几个必须知道的点:

  • 释放锁要用 Lua:把「判断 value + 删除」做成原子操作,否则判断完到删除之间锁可能已过期并被别人拿到。
  • 锁续期:业务还没跑完锁就过期了,Redisson 的看门狗(Watch Dog)会在后台自动续期。
  • 可重入:同一线程可以重复获取同一把锁,Redisson 支持。
  • 主从丢锁:主节点宕机、锁还没同步到从节点就丢了,可以用 RedLock 缓解,但 RedLock 本身有争议。

使用要点:锁要按业务唯一键来加,保证同一资源串行处理;释放必须放在 finally 里且校验持有者;锁的过期时间要和业务耗时匹配,否则业务没跑完锁就没了,Redisson 的 tryLock + 看门狗就是为这一点兜底的。


8. Redis 集群:主从、哨兵、Cluster ★★★ ​

  • 主从复制:主写从读,从节点做备份和读写分离,但主挂了需要人工切换。
  • 哨兵(Sentinel):监控主从节点,主挂了自动选举新主并通知客户端,解决高可用。
  • Cluster:数据按 16384 个槽分片到多个主节点,每个主节点再带从节点,解决容量和水平扩展,自带故障转移。

一句话区分:哨兵解决「单个主节点的高可用」,Cluster 解决「要多主分片扩展」。规模不大时主从 + 哨兵就够用;需要注意主从切换期间存在分布式锁丢失的窗口,锁还没同步到从节点就切了。


9. 缓存预热、缓存降级、热点 key ★★☆ ​

  • 缓存预热:系统启动时提前把热点数据加载进缓存,避免冷启动时大量请求直接打库。做法可以是启动时自动加载、定时刷新,或上线后手工刷新。
  • 缓存降级:缓存或数据库压力过大时,不查库而是返回兜底数据,或直接限流,保证核心链路可用。
  • 热点 key:访问过度集中在少数 key 上,单分片压力大;可以做本地缓存 + Redis 二级缓存分流。

10. 为什么用消息队列 ★★★ ​

三大作用:

  • 解耦:生产者只管发消息,不直接依赖下游有哪些消费者。
  • 异步:非核心流程异步执行,主链路更快返回。
  • 削峰:高峰请求先入队,消费者按自己的能力消费,把突发流量抹平,保护数据库等下游。

代价也要说清楚:

  • 系统复杂度上升,多了一个必须运维的组件;
  • 引入消息丢失、重复消费带来的一致性问题,消费端必须做幂等;
  • 业务强依赖 MQ 的可用性,MQ 挂了整条链路都受影响。

11. 消息丢失怎么解决 ★★★ 【简历亮点,高频追问】 ​

分三个环节分别堵:

  1. 生产者丢:发送时网络异常或 broker 没收到。用发送确认(ack) + 重试,必要时用事务消息保证本地事务与发消息的一致性。
  2. MQ 自身丢:消息还在内存里没持久化就宕机。开持久化 + 多副本(主从),并让 ack 在副本同步后再返回。
  3. 消费者丢:消息已投递但还没处理完消费者就挂了。关掉自动提交位移,改成处理完手动 ack / 提交位移。

核心是保证「至少一次」投递;对支付等关键消息,宁可重复也不能丢,重复交给消费端幂等兜住。


12. 消息重复消费怎么解决 ★★★ 【简历亮点,高频追问】 ​

只要保证「至少一次」投递,加上发送端的网络重试,重复是天然的、避不开的。MQ 本身保证不了不重复,只能靠消费端做幂等。

常用幂等手段:

  • 数据库唯一索引:插入冲突就忽略。
  • Redis setnx:记录处理过的消息 id,重复消息直接跳过。
  • 状态机:例如订单已支付/已结束,再收到同样的消息就跳过。

原则就是不假设消息只来一次,把处理设计成可重入的。


13. 消息顺序消费 ★★☆ ​

要保证顺序只有两点:同一业务的消息发到同一个分区 / 队列,并且由同一个消费者单线程按顺序消费。

  • Kafka 只保证分区内有序,不同分区之间不保证;所以把业务 key 哈希到同一分区,就能保证同一订单 / 同一资源内有序。
  • 全局有序只能用一个队列 + 单消费者,性能很差,一般没必要。

14. 消息积压怎么解决 ★★☆ ​

先定位,再扩容,常见做法:

  1. 先看消费者是不是卡住了:消费线程死锁、下游超时、异常重试风暴都会表现为积压,不排查就扩容只是浪费机器。
  2. 加消费者 / 提并发:增加消费实例或线程数。
  3. 优化消费逻辑:批处理、减少同步 IO 和远程调用。
  4. 临时分流:把积压消息转到新 topic,用更多消费者快速消化,之后再恢复正常链路。

核心是先定位瓶颈再扩容,并配合消费延迟监控告警。


15. 死信队列、延迟消息 ★★☆ ​

  • 死信队列:消息重试到上限、过期、或队列满了之后转入死信队列,用来告警和人工排查补偿,避免消息静默消失。
  • 延迟消息:消息延迟一段时间后才投递。RocketMQ 原生支持;RabbitMQ 用 TTL + 死信队列模拟;Kafka 没有原生支持,要自己实现。

典型用途是订单超时取消:下单时投一条延迟消息,到时未支付就取消。但只靠延迟消息不够稳,要有定时任务兜底补偿。


16. Kafka、RocketMQ、RabbitMQ 对比 ★★☆ ​

  • Kafka:大数据、日志、高吞吐场景,性能最强,但事务消息、延迟消息能力相对弱。
  • RocketMQ:面向业务消息,支持事务消息、原生延迟消息,Java 生态友好。
  • RabbitMQ:通用消息队列,走标准 AMQP 协议,路由灵活、生态成熟,吞吐中等。
  • 吞吐量上 Kafka、RocketMQ 明显高于 ActiveMQ、RabbitMQ;延迟最低的是 RabbitMQ;Kafka 定位偏日志 / 流处理,业务消息更推荐 RabbitMQ、RocketMQ。

选型看吞吐量、是否需要事务消息和延迟消息、团队技术栈,不能只看单项指标。


补充(原文详解)

对于吞吐量来说kafka和RocketMQ支撑高吞吐,ActiveMQ和RabbitMQ比他们低一个数量级。对于延迟量来说RabbitMQ是最低的。

1.从社区活跃度

按照目前网络上的资料,RabbitMQ、activeM、ZeroMQ 三者中,综合来看,RabbitMQ 是首选。

2.持久化消息比较

ActiveMq 和RabbitMq 都支持。持久化消息主要是指我们机器在不可抗力因素等情况下挂掉了,消息不会丢失的机制。

3.综合技术实现

可靠性、灵活的路由、集群、事务、高可用的队列、消息排序、问题追踪、可视化管理工具、插件系统等等。

RabbitMq / Kafka 最好,ActiveMq 次之,ZeroMq 最差。当然ZeroMq 也可以做到,不过自己必须手动写代码实现,代码量不小。尤其是可靠性中的:持久性、投递确认、发布者证实和高可用性。

4.高并发

毋庸置疑,RabbitMQ 最高,原因是它的实现语言是天生具备高并发高可用的erlang 语言。

5.比较关注的比较, RabbitMQ 和 Kafka

RabbitMq 比Kafka 成熟,在可用性上,稳定性上,可靠性上, RabbitMq 胜于 Kafka (理论上)。

另外,Kafka 的定位主要在日志等方面, 因为Kafka 设计的初衷就是处理日志的,可以看做是一个日志(消息)系统一个重要组件,针对性很强,所以如果业务方面还是建议选择 RabbitMq。

还有就是,Kafka 的性能(吞吐量、 TPS)比RabbitMq 要高出来很多。

17. 缓存设计模式 ★★★ ​

  • Cache Aside(最常用):读时先查缓存,未命中查库回填;写时先更库再删缓存。逻辑在应用层,缓存只负责存数据。
  • Read Through:查库回填的逻辑封装在缓存组件里,应用只和缓存交互,不感知数据库。
  • Write Through:写时缓存和库同步更新,一致性好,但每次写都多一次库操作,写开销大。
  • Write Behind:只写缓存,异步批量刷库,性能最高,但缓存宕机时最可能丢数据。

一般业务选 Cache Aside;Write Through 适合写少且一致性要求高的场景;Write Behind 适合能容忍丢数据的高写入场景。


18. Redis Pipeline 的好处 ★★☆ ​

把多条命令打包一次发过去、一次读回结果,把 N 次网络往返(RTT)压缩成 1 次,大幅减少网络开销,适合批量写、批量查。

注意:Pipeline 不是原子操作,打包的命令之间可能被其他客户端的命令插进来;要原子性必须配事务 MULTI/EXEC。


19. Redis 和 HashMap 的区别 ★★☆ ​

  • Redis 是独立部署的分布式缓存服务,客户端走网络访问,能跨进程、跨机器共享数据,并且有持久化、过期、淘汰等能力。
  • HashMap 是 JVM 里的本地数据结构,单机单进程,重启就没了,也不能在多个实例之间共享。

所以多实例部署的服务,需要共享的数据必须放 Redis;本地 HashMap 只能放只读的配置类数据,否则各实例之间数据不一致。


20. Redis、本地缓存(Caffeine)、Memcached 怎么选 ★★☆ ​

  • 需要跨服务共享、要持久化、要丰富的数据结构 → Redis。
  • 单机内的热点数据、要纳秒级低延迟、不需要共享(配置、字典)→ 本地缓存。
  • 常见组合:本地缓存做一层,Redis 做二层,减少网络开销。
  • Memcached 只有简单 KV、无持久化,现在基本被 Redis 取代。

组合使用时要注意两级缓存的一致性问题,本地缓存的更新和失效需要额外机制同步。


21. Redis 内存用完了会发生什么 ★★★ 【简历亮点,高频追问】 ​

行为由淘汰策略决定:

  • 默认 noeviction:内存达到上限后,写命令直接报错,读命令还能正常执行。
  • 配了 allkeys-lru 等策略:会淘汰旧 key 腾出空间继续写。

最危险的情况是没配 maxmemory:Redis 会一直申请内存,直到把宿主机内存吃光、被系统 OOM Killer 杀掉,还可能拖垮同机其他服务,所以线上一定要显式设置 maxmemory 和淘汰策略。


22. MQ 部署单机还是集群、高可用怎么保证 ★★★ ​

生产环境必须集群,单机没有容灾能力。各 MQ 的高可用方式:

  • Kafka:partition 做多副本(leader + follower),leader 挂了从 ISR 中自动选主;生产者的 acks 决定有多少副本确认后才算发送成功。
  • RocketMQ:主从复制 + NameServer 集群,主节点挂了从节点接管。
  • RabbitMQ:镜像队列或 Quorum Queue,保证队列在多个节点有副本。

配置上副本数要设够、acks 要配对,否则 broker 挂了消息会丢,而且平时不容易发现。


23. 消息队列的延时、过期失效问题 ★★★ ​

现象:消息设了 TTL 又一直没有被消费,过期后进入死信,对应的业务就静默丢失了,而且很难发现。

应对:

  • 死信队列兜底:对进入死信的消息做补偿处理或告警。
  • 监控积压:消费延迟、积压量要告警,及时扩容。
  • TTL 不要设得过短:给消费留足重试时间。
  • 关键业务不要只依赖 MQ 的 TTL,用定时任务扫表补偿,保证消息过期了业务也不会丢。

24. 如果让你写一个消息队列,如何架构 ★★★ ​

按模块拆解:

  1. 存储:消息顺序追加写日志,配合页缓存和零拷贝,把磁盘顺序读写和网络发送的吞吐做到最高。
  2. 分区:一个 topic 拆多个 partition,实现并行读写,同时保证分区内有序。
  3. 网络层:用高性能网络框架(如 Netty)做长连接接入、协议编解码和连接管理。
  4. 投递与位移管理:记录每个消费者组的 offset,支持按位点拉取和重置。
  5. 多副本高可用:leader + follower 副本,leader 故障自动选主。
  6. 可靠投递:生产端 ack + 重试,消费端处理完再提交位移,保证至少一次。

Kafka 快的核心就是顺序写 + 页缓存 + 零拷贝这三点。


25. Redis 慢查询怎么找、怎么优化 ★★★ 【简历亮点,高频追问】 ​

定位靠慢日志:

  • 配置 slowlog-log-slower-than 设置慢查询阈值,超过阈值的命令记入慢日志;
  • 用 slowlog get 查看慢命令、耗时和参数;
  • 也可以用 latency monitor 观察延迟事件。

优化从命令和参数入手:

  • 禁用 keys *,遍历改用 scan;
  • 拆分大 key,避免单次操作搬运过多数据;
  • 删除大 key 用 unlink 异步释放;
  • 批量操作用 pipeline 减少网络往返;
  • 调整持久化策略,减少 fork、AOF 重写带来的抖动。

Redis 慢的根因是单线程:一个慢命令会卡住后面所有请求。典型现象是接口偶发变慢,slowlog 里能看到是在操作大 key,拆 key 并把删除改成 unlink 后恢复。


26. Redis 性能优化 ★★★ ​

按层来看:

  • 内存层:配好淘汰策略、控制大 key、设置合理的 maxmemory。
  • 命令层:避开 O(n) 命令,遍历用 scan,批量用 pipeline。
  • 网络层:用 pipeline 减少往返;Redis 6.0 之后可以开启多线程 IO。
  • 持久化层:AOF 用 everysec,及时重写避免日志膨胀。
  • 架构层:主从做读写分离、Cluster 做分片扩展、热点 key 用本地缓存挡一层。
  • 参数层:设好 maxmemory 和连接池。

附:原始题库(旧版逐题,与上文重叠,可折叠跳过) ​

展开查看原始题库

27. 为什么要用缓存 ​

使用缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,带来更好的性

能,带来更高的并发量。 Redis 的读写性能比 Mysql 好的多,我们就可以把 Mysql 中的热点数据缓存到 Redis 中,提升读取性能,同时也减轻了 Mysql 的读取压力。

欢迎关注微信公众号:Java后端技术全栈

28. 使用 Redis 有哪些好处? ​

具有以下好处:

读取速度快,因为数据存在内存中,所以数据获取快;

支持多种数据结构,包括字符串、列表、集合、有序集合、哈希等;

支持事务,且操作遵守原子性,即对数据的操作要么都执行,要么都不支持; 还拥有其他丰富的功能,队列、主从复制、集群、数据持久化等功能。

29. 什么是 Redis? ​

Redis 是一个开源(BSD 许可)、基于内存、支持多种数据结构的存储系统,可以作为数据库、缓存和消息中间件。它支持的数据结构有字符串(strings)、哈希(hashes)、列表(lists)、集合 ( sets)、有序集合(sorted sets)等,除此之外还支持 bitmaps、 hyperloglogs 和地理空间(

geospatial)索引半径查询等功能。

它内置了复制(Replication)、 LUA 脚本(Lua scripting)、 LRU 驱动事件(LRU eviction)、事务(Transactions)和不同级别的磁盘持久化(persistence)功能,并通过 Redis 哨兵(哨兵)和集群(Cluster)保证缓存的高可用性(High availability)。

Redis 而不是用Memcache 呢?4,为什么使用

Memcache 与Redis 区别。这时候肯定想到的就是做一个

Memcache 都是将数据存放在内存中,都是内存数据库。不过Memcache 还可用于缓存Redis 和

其他东西,例如图片、视频等等。

Memcache 仅支持key-value结构的数据类型,Redis不仅仅支持简单的key-value类型的数据, 同时还提供list,set,hash等数据结构的存储。

虚拟内存–Redis 当物理内存用完时,可以将一些很久没用到的value 交换到磁盘

分布式–设定magent 做一主多从;Memcache 集群,利用Redis 可以做一主多从。都可以一主一

从

存储数据安全–Memcache 挂掉后,数据没了;Redis 可以定期保存到磁盘(持久化)

Redis 的单个value最大512m。1m,Memcache 的单个value最大

灾难恢复–aof 恢复Redis 数据丢失后可以通过Memcache 挂掉后,数据不可恢复;

Redis 原生就支持集群模式,Redis3.0 版本中,官方便能支持Cluster模式了,Memcached 没

有原生的集群模式,需要依赖客户端来实现,然后往集群中分片写入数据。

nignx。而RedisMemcached 网络IO模型是多线程,非阻塞IO复用的网络模型,原型上接近于

AeEvent 事件处理框架,主要实现类使用单线程的IO复用模型,自己封装了一个简单的

selectepoll,kqueue 和,更接近于Apache早期的模式。

30. 说说Redis 的线程模型 ​

这问题是因为前面回答问题的时候提到了Redis 是基于非阻塞的IO复用模型。如果这个问题回

答不上来,就相当于前面的回答是给自己挖坑,因为你答不上来,面试官对你的印象可能就要打点折扣了。

Redis 内部使用文件事件处理器file event handler,这个文件事件处理器是单线程的,所以

Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  1. 多个 socket 。

  2. IO 多路复用程序。

  3. 文件事件分派器。

  4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

来看客户端与Redis 的一次通信过程:

说说Redis 的线程模型

Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个Socket01 向下面来大致说一下这个图:

  1. 客户端

AE_READABLE 事件与命令请求处理器关联。的Socket01,并将该 Socket01AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的

Socket01 会产生Redis 中的2. 假设此时客户端发送了一个 set key value 请求,此时

AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取Socket01 的AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面

AE_WRITABLE 事件与令Socket01 的Socket01 的 set key value 并在自己内存中完成 set key value 的设置。操作完成后,它会将

回复处理器关联。

Socket01 会产生一个Redis 中的3. 如果此时客户端准备好接收返回结果了,那么

java
AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复
AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复

ok,之后解除Socket01 的处理器对Socket01 输入本次操作的一个结果,比如

java
AE_WRITABLE 事件与命令回复处理器的关联。
AE_WRITABLE 事件与命令回复处理器的关联。

这样便完成了一次通信。 不要怕这段文字,结合图看,一遍不行两遍,实在不行可以网上查点资料结合着看,一定要搞清楚,否则前面吹的牛逼就白费了。

31. 为什么 Redis 需要把所有数据放到内存中? ​

Redis 将数据放在内存中有一个好处,那就是可以实现最快的对数据读取,如果数据存储在硬盘

中,磁盘 I/O 会严重影响 Redis 的性能。而且 Redis 还提供了数据持久化功能,不用担心服务器重启对内存中数据的影响。其次现在硬件越来越便宜的情况下,Redis 的使用也被应用得越来越多, 使得它拥有很大的优势。

32. Redis 的同步机制了解是什么? ​

Redis 支持主从同步、从从同步。如果是第一次进行主从同步,主节点需要使用 bgsave 命令,再将后续修改操作记录到内存的缓冲区,等 RDB 文件全部同步到复制节点,复制节点接受完成后将

RDB 镜像记载到内存中。等加载完成后,复制节点通知主节点将复制期间修改的操作记录同步到复制节点,即可完成同步过程。

33. pipeline 有什么好处,为什么要用 pipeline? ​

使用 pipeline(管道)的好处在于可以将多次 I/O 往返的时间缩短为一次,但是要求管道中执行的指令间没有因果关系。

用 pipeline 的原因在于可以实现请求/响应服务器的功能,当客户端尚未读取旧响应时,它也可以处理新的请求。如果客户端存在多个命令发送到服务器时,那么客户端无需等待服务端的每次响应才能执行下个命令,只需最后一步从服务端读取回复即可。

34. 说一下Redis 有什么优点和缺点 ​

优点

速度快:因为数据存在内存中,类似于HashMap,HashMap 的优势就是查找和操作的时间复杂

度都是O (1)。

支持丰富的数据结构:支持 String,List,Set,Sorted Set,Hash 五种基础的数据结构。

持久化存储:Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的万一 Redis 挂掉,数据会消失掉

Redis高可用:内置Redis Sentinel,提供高可用方案,实现主从故障自动转移。 内置

ClusterRedis 规模。,提供集群方案,实现基于槽的分片方案,从而支持更大的

丰富的特性:Key过期、计数、分布式锁、消息队列等。

缺点

由于Redis 是内存数据库,所以,单台机器,存储的数据量,跟机器本身的内存大小。虽然11Redis 本身有 Key 过期策略,但是还是需要提前预估和节约内存。如果内存增长过快,需要定期删除数据。如果进行完整重同步,由于需要生成RDB 文件,并进行传输,会占用主机的 CPU,并会消耗Redis 2.8 版本,已经有部分重同步的功能,但是还是有可能有完整重同步现网的带宽。不过的。比如,新上线的备机。Redis修改配置文件,进行重启,将硬盘中的数据加载进内存,时间比较久。在这个过程中,不能提供服务。Redis 缓存刷新策略有哪些?Redis 持久化方式有哪些?以及有什么区别?Redis 提供两种持久化机制AOF 机制:RDB 和只有一个文件dump.rdb,方便持久化。缺点:容灾性好,一个文件可以保存到安全的磁盘。性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了Redis 的高性能)AOF 的启动效率更高。相对于数据集大时,比Redis 发生故障,会发生数据RDB 是间隔一段时间进行持久化,如果持久化之间丢失。所以这种方式更适合数据要求不严谨的时候AOF=Append-only file 持久化方式

说一下Redis 有什么优点和缺点

12,

java
RDB 持久化方式
RDB 持久化方式

是指用数据集快照的方式半持久化模式)记录redis 数据库的所有键值对,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

优点:

数据安全性低。

AOFflushall )

是指所有的命令行记录以文件。Redis 命令请求协议的格式完全持久化存储,保存为

优点:

( 1)数据安全,AOF 持久化可以配置AOF 文件中一次。appendfsync 属性,有 always,每进行一次命令操作就记录到

redis-check-aof 工具解决数据( 2)通过 append 模式写文件,即使中途服务器宕机,可以通过

一致性问题。

AOF 机制的 rewrite 模式。( 3)AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的

缺点:

( 1)AOF 文件比RDB 启动效率低。RDB 文件大,且恢复速度慢。

( 2)数据集大的时候,比

35. 持久化有两种,那应该怎么选择呢? ​

  1. 不要仅仅使用AOF 做冷备没有做冷备的恢AOF ,因为那样有两个问题,第一,你通过RDBRDB ,因为那样会导致你丢失很多数据。

  2. 也不要仅仅使用

AOF 这种复杂的备RDB 每次简单粗暴生成数据快照,更加健壮,可以避免复速度更快; 第二,

Redis 支持同时开启开启两种持久化方式,我们可以综合使用RDB 两种持久化机制,AOF 和份和恢复机制的 bug。

RDB 来做不同程度的冷备,在AOF 来保证数据不丢失,作为数据恢复的第一选择; 用AOF用

RDB 来进行快速的数据恢复。文件都丢失或损坏不可用的时候,还可以使用

AOF 来重新Redis 重启的时候,会使用AOF 两种持久化机制,那么在RDB 和4. 如果同时使用

构建数据,因为AOF 中的数据更加完整。

36. 怎么使用 Redis 实现消息队列? ​

rpush 生产消息,lpop 消费消息。当lpop 没有消息的时候,要适当一般使用 list 结构作为队列,

sleep 一会再重试。

blpop,在没有消息的时候,它会面试官可能会问可不可以不用 sleep 呢?list 还有个指令叫

阻塞住直到消息到来。

面试官可能还问能不能生产一次消费多次呢?使用 pub / sub 主题订阅者模式,可以实现 1:N 的消息队列。

等。面试官可能还问 pub / sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq

面试官可能还问Redis 如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用

zadd 来生产消息,消费者用sortedset,拿时间戳作为 score,消息内容作为 key 调用

zrangebyscore 指令获取Redis 如何实现延时队列?N 秒之前的数据轮询进行处理。

面试扩散:很多面试官上来就直接这么问:

37. 说说你对Redis事务的理解 ​

什么是 Redis 事务?原理是什么?

Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。

它的原理是先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。

Redis 事务的注意点有哪些?

需要注意的点有:

Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;

Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。

Redis 事务为什么不支持回滚?

Redis 的事务不支持回滚,但是执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层

面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。

38. 什么是 bigkey?会存在什么影响? ​

bigkey 是指键值占用内存空间非常大的 key。例如一个字符串 a 存储了 200M 的数据。

bigkey 的主要影响有:

网络阻塞;获取 bigkey 时,传输的数据量比较大,会增加带宽的压力。

超时阻塞;因为 bigkey 占用的空间比较大,所以操作起来效率会比较低,导致出现阻塞的可能性增加。

2.Redis3.Codis

导致内存空间不平衡;一个 bigkey 存储数据量比较大,同一个 key 在同一个节点或服务器中存储,会造成一定影响。

39. 熟悉哪些Redis 集群模式? ​

Redis Sentinel1.

Redis 足以支撑业务。Redis Sentinel,单主体量较小时,选择

Redis Cluster

Cluster,通过分片,使用更多内Redis 官方提供的集群化方案,体量较大时,选择

存。

Twemprox

Twtter 开源的一个Redis 和Redis 和Memcached 代理服务器,主要用于管理Twemprox 是

Memcached 集群,减少与Cache 服务器直接连接的数量。

Codis 负责将指令转发到后面的Codis 发送指令时,Codis 是一个代理中间件,当客户端向

Redis 实例,也可以启Codis 实例可以连接多个Redis 来执行,并将结果返回给客户端。 一个

QPS 需求,还能Codis 节点都是对等的,这样可以增加整体的动多个Codis 实例来支撑,每个

起到容灾功能。

  1. 客户端分片

在Redis 实例,在代码层,对 Key 进行 hash 计算,然后去对应的Redis Cluster 还没出现之前使用较多,现在基本很少热你使用了,在业务代码层实现,起几个毫无关联的Redis 实例操作数据。这种方式对 hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案, 数据震荡后的自动脚本恢复,实例的监控,等等。

40. 是否使用过Redis Cluster 集群,集群的原理是什么? ​

使用过 Redis 集群,它的原理是:

所有的节点相互连接

集群消息通信通过集群总线通信,集群总线端口大小为客户端服务端口 + 10000(固定值) 节点与节点之间通过二进制协议进行通信

客户端和集群节点之间通信和通常一样,通过文本协议进行

集群节点不会代理查询

数据按照 Slot 存储分布在多个 Redis 实例上

集群节点挂掉会自动故障转移

可以相对平滑扩/缩容节点

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时, redis 先对 key 使用crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0~16383 之间的哈希槽, redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

41. Redis Cluster 集群方案什么情况下会导致整个集群不可用? ​

Redis 没有使用哈希一致性算法,而是使用哈希槽。 Redis 中的哈希槽一共有 16384 个,计算给定密钥的哈希槽,我们只需要对密钥的 CRC16 取摸 16384。假设集群中有 A、 B、C 三个集群节点, 不存在复制模式下,每个集群的节点包含的哈希槽如下:

节点 A 包含从 0 到 5500 的哈希槽;

节点 B 包含从 5501 到 11000 的哈希槽;

节点 C 包含从 11001 到 16383 的哈希槽;

这时,如果节点 B 出现故障,整个集群就会出现缺少 5501 到 11000 的哈希槽范围而不可用。

42. Redis 集群架构模式有哪几种? ​

Redis 集群架构是支持单节点单机模式的,也支持一主多从的主从结构,还支持带有哨兵的集群部署模式。

43. 说说 Redis 哈希槽的概念? ​

Redis 集群并没有使用一致性 hash,而是引入了哈希槽的概念。Redis 集群有 16384( 2^14)个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

44. Redis 常见性能问题和解决方案有哪些? ​

Redis 常见性能问题和解决方案如下:

Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件;

如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次; 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内; 尽量避免在压力很大的主库上增加从库;

主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-

Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。

补充(原文详解)

(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件 (2) 如果数据比较重

要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次 (3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内 (4) 尽量避免在压力很大的主库上增加从库 (5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3 …

45. 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来? ​

我们可以使用 keys 命令和 scan 命令,但是会发现使用 scan 更好。

  1. 使用 keys 命令

直接使用 keys 命令查询,但是如果是在生产环境下使用会出现一个问题,keys 命令是遍历查询的,查询的时间复杂度为 O(n),数据量越大查询时间越长。而且 Redis 是单线程,keys 指令会导致线程阻塞一段时间,会导致线上 Redis 停顿一段时间,直到 keys 执行完毕才能恢复。这在生产环境是不允许的。除此之外,需要注意的是,这个命令没有分页功能,会一次性查询出所有符合条件的 key 值,会发现查询结果非常大,输出的信息非常多。所以不推荐使用这个命令。

  1. 使用 scan 命令

scan 命令可以实现和 keys 一样的匹配功能,但是 scan 命令在执行的过程不会阻塞线程,并且查找的数据可能存在重复,需要客户端操作去重。因为 scan 是通过游标方式查询的,所以不会导致 Redis 出现假死的问题。 Redis 查询过程中会把游标返回给客户端,单次返回空值且游标不为 0,则说明遍历还没结束,客户端继续遍历查询。 scan 在检索的过程中,被删除的元素是不会被查询出来

的,但是如果在迭代过程中有元素被修改, scan 不能保证查询出对应元素。相对来说, scan 指令查找花费的时间会比 keys 指令长。

46. 如果有大量的 key 需要设置同一时间过期,一般需要注意什么? ​

如果有大量的 key 在同一时间过期,那么可能同一秒都从数据库获取数据,给数据库造成很大的压力,导致数据库崩溃,系统出现 502 问题。也有可能同时失效,那一刻不用都访问数据库,压力不够大的话,那么 Redis 会出现短暂的卡顿问题。所以为了预防这种问题的发生,最好给数据的过期时间加一个随机值,让过期时间更加分散。

47. 什么情况下可能会导致 Redis 阻塞? ​

Redis 产生阻塞的原因主要有内部和外部两个原因导致:

内部原因

如果 Redis 主机的 CPU 负载过高,也会导致系统崩溃;

数据持久化占用资源过多;

对 Redis 的 API 或指令使用不合理,导致 Redis 出现问题。

外部原因

外部原因主要是服务器的原因,例如服务器的 CPU 线程在切换过程中竞争过大,内存出现问题、网

络问题等。

48. 缓存和数据库谁先更新呢? ​

解决方案

  1. 写请求过来,将写请求缓存到缓存队列中,并且开始执行写请求的具体操作(删除缓存中的数 据,更新数据库,更新缓存)。

  2. 如果在更新数据库过程中,又来了个读请求,将读请求再次存入到缓存队列(可以搞n个队

列,采用key的hash值进行队列个数取模hash%n,落到对应的队列中,队列需要保证顺序性) 中,顺序性保证等待队列前的写请求执行完成,才会执行读请求之前的写请求删除缓存失败, 直接返回,此时数据库中的数据是旧值,并且与缓存中的数据是一致的,不会出现缓存一致性的问题。

  1. 写请求删除缓存成功,则更新数据库,如果更新数据库失败,则直接返回,写请求结束,此时 数据库中的值依旧是旧值,读请求过来后,发现缓存中没有数据, 则会直接向数据库中请求, 同时将数据写入到缓存中,此时也不会出现数据一致性的问题。

  2. 更新数据成功之后,再更新缓存,如果此时更新缓存失败,则缓存中没有数据,数据库中是新 值 ,写请求结束,此时读请求还是一样,发现缓存中没有数据,同样会从数据库中读取数据, 并且存入到缓存中,其实这里不管更新缓存成功还是失败, 都不会出现数据一致性的问题。

上面这方案解决了数据不一致的问题,主要是使用了串行化,每次操作进来必须按照顺序进行。如果某个队列元素积压太多,可以针对读请求进行过滤,提示用户刷新页面,重新请求。

潜在的问题,留给大家自己去想吧,因为这个问题属于发散性。

1,请求时间过长,大量的写请求堆压在队列中,一个读请求来得等都写完了才可以获取到数据。

2,读请求并发高

3,热点数据路由问题,导致请求倾斜。

49. 怎么提高缓存命中率? ​

主要常用的手段有:

提前加载数据到缓存中;

增加缓存的存储空间,提高缓存的数据;

调整缓存的存储数据类型;

提升缓存的更新频率。

50. Redis 如何解决 key 冲突? ​

Redis 如果 key 相同,后一个 key 会覆盖前一个 key。如果要解决 key 冲突,最好给 key 取好名区分开,可以按业务名和参数区分开取名,避免重复 key 导致的冲突。

51. Redis 报内存不足怎么处理? ​

Redis 内存不足可以这样处理:

修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存;

设置缓存淘汰策略,提高内存的使用效率;

使用 Redis 集群模式,提高存储量。

52. 说说Redis持久化机制 ​

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。 实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。

RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。) AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。 当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

53. 热点数据和冷数据是什么 ​

热点数据,缓存才有价值对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存, 不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。 对于热点数据, 比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例

子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。 数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。 那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

54. Memcache与Redis的区别都有哪些? ​

1)、存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。

Redis有部份存在硬盘上, redis可以持久化其数据 2)、数据支持类型 memcached所有的值均是简单的字符串, redis作为其替代者,支持更为丰富的数据类型,提供list,set,zset,hash等数据结构的存储 3)、使用底层模型不同它们之间底层实现方式以及与客户端之间通信的应用协议不一

样。 Redis直接自己构建了VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。 4). value 值大小不同:Redis 最大可以达到 1gb;memcache 只有 1mb。 5)redis的速度比memcached快很多 6)Redis支持数据的备份,即master-slave模式的数据备份。

55. redis的数据类型,以及每种数据类型的使用场景 ​

回答:一共五种 (一)String 这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。 一般做一些复杂的计数功能的缓存。 (二)hash 这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以 cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。 (三)list 使用 List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于 redis的分页功能,性能极佳,用户体验好。本人还用一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。 LIST可以很好的完成排队,先进先出的原则。 (四)set 因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。 另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好, 自己独有的喜好等功能。 (五)sorted set sorted set多了一个权重参数score,集合中的元素能够按

score进行排列。可以做排行榜应用,取TOP N操作。

56. redis的过期策略以及内存淘汰机制 ​

redis采用的是定期删除+惰性删除策略。 为什么不用定时删除策略? 定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略. 定期删除+惰性删除是如何工作的呢? 定期删除, redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,

redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查, redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。 于是,惰性删除派上用场。也就是说在你获取某个key的时候, redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。 采用定期删除+惰性删除就没其他问题了么? 不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这

样, redis的内存会越来越高。那么就应该采用内存淘汰机制。 在redis.conf中有一行配置

java
maxmemory-policy volatile-lru
maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己) volatile-lru:从已设置过期时间的数据集( server.db[i].expires)中挑选最近最少使用的数据淘汰 volatile-ttl:从已设置过期时间的数据集( server.db[i].expires)中挑选将要过期的数据淘汰 volatile-random:从已设置过期时间的数据集( server.db[i].expires)中任意选择数据淘汰 allkeys-lru:从数据集

( server.db[i].dict)中挑选最近最少使用的数据淘汰 allkeys-random:从数据集

( server.db[i].dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据,新写入操作会报错 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile- random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

57. 为什么Redis的操作是原子性的,怎么保证原子性的? ​

对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。

Redis的操作之所以是原子性的,是因为Redis是单线程的。 Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。 多个命令在并发中也是原子性的吗? 不一定, 将get和set改成单命令操作,incr。使用Redis的事务,或者使用Redis+Lua==的方式实现.

58. 了解Redis的事务吗? ​

Redis事务功能是通过MULTI、 EXEC、 DISCARD和WATCH 四个原语实现的 Redis会将一个事务中的所有命令序列化,然后按顺序执行。 1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”,所以 Redis 的内部可以保持简单且快速。 2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行; 3.如果在一个事务中出现运行错误,那么正确的命令会被执行。

1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。 2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回

值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil。 3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。 4)WATCH 命令可以为 Redis 事务提供 check-and-set( CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

59. Redis 的数据类型及使用场景 ​

String

最常规的 set/get 操作,Value 可以是 String 也可以是数字。一般做一些复杂的计数功能的缓存。

Hash

这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。我在做单点登录的时候,就是用这种数据结构存储用户信息,以 CookieId 作为 Key,设置 30 分钟为缓存过期时间,能很好的模拟出类似 Session 的效果。

List

使用 List 的数据结构,可以做简单的消息队列的功能。另外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

Set

因为 Set 堆放的是一堆不重复值的集合。所以可以做全局去重的功能。我们的系统一般都是集群部署,使用 JVM 自带的 Set 比较麻烦。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

Sorted Set

Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score进行排列。可以做排行榜应用, 取 TOP(N) 操作。 Sorted Set 可以用来做延时任务。

60. 为什么要使用MQ ​

核心:解耦,异步,削峰

1)解耦:A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃......A 系统跟其它各种乱七八糟的系统严重耦

合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用

MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。

就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。

( 2)异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 +

450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请

求。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms。

( 3)削峰:减少高峰时期对服务器压力。

I 欢迎关注微信公众号:Java后端技术全栈

61. MQ有什么优缺点 ​

优点上面已经说了,就是在特殊场景下有其对应的好处,解耦、异步、削峰。

缺点有以下几个:

系统可用性降低系统引入的外部依赖越多,越容易挂掉。万一 MQ 挂了,MQ一挂,整套系统崩溃,你不就完了?

系统复杂度提高硬生生加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况? 怎么保证消息传递的顺序性?问题一大堆。

一致性问题 A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是

BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

62. 如何保证高可用的? ​

RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。 RabbitMQ 有三种模式:单机模式、普通集群模式、 镜像集群模式。

单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式

普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的

queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。 这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。 RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机

了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ一个 queue 的数据都是放在一个节点里的,镜像集群下, 也是每个节点都放这个 queue 的完整数据。

Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个

topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。 Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有

replica 分布在不同的机器上,这样才可以提高容错性。因为如果某个 broker 宕机了,没事儿,那个 broker上面的 partition 在其他机器上都有副本的,如果这上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,

接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当

然,这只是其中一种模式,还可以适当调整这个行为)消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

63. 如何保证消息的可靠传输?如果消息丢了怎么办 ​

数据的丢失问题,可能出现在生产者、 MQ、消费者中

生产者丢失:生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启

RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。吞吐量会下来,因为太耗性能。所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启confirm模式,在生产者那里设置开启confirm模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给

你回传一个ack消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个 nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。事务机制和 cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是

confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。所以一般在生产者这块避免数据丢失,都是用confirm机制的。

MQ中丢失:就是 RabbitMQ 自己弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写

入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。设置持久化有两个步骤:创建 queue 的时候将其设置为持久化,这样就可以保证

RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,

RabbitMQ 挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。注意,哪怕是你给

RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。

消费端丢失:你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了, RabbitMQ 认为你都消费了,这数据就丢了。这个时候得用 RabbitMQ 提供的ack机制,简单来说,就是你关闭 RabbitMQ 的自动ack,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里ack一把。这样的话,如果你还没处理完,不就没有ack?那

RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。

如何保证消息的可靠传输?如果消息丢了怎么办

64. 如何保证消息的顺序性 ​

先看看顺序会错乱的场景:RabbitMQ:一个 queue,多个 consumer,这不明显乱了;

如何保证消息的顺序性

解决:

如何保证消息的顺序性

65. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? ​

消息积压处理办法:临时紧急扩容:

先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。 新建一个

topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。 MQ中消息失效:假设你用的是 RabbitMQ,

RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回

来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。

66. 让你来设计一个消息队列,你会怎么设计 ​

比如说这个消息队列系统,我们从以下几个角度来考虑一下:

首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个

partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition, 然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?

其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。

其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。

能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。

附:一句话速记(可以跳过 ★☆☆) ​

  • Redis 事务(MULTI/EXEC):多条命令打包执行,但不支持回滚,某条失败其余照常执行,所以基本没人拿它当事务用。
  • Redis 发布订阅:能广播但消息不持久化、不可靠,业务里一般用 Kafka 代替。
  • Memcached:纯内存 KV、多线程、无持久化,现在基本被 Redis 取代,了解概念即可。