总结一些八股答案

2575 字
13 分钟
总结一些八股答案

怎么保证 MQ 消息顺序性#

严格来说,无法保证。首先,这个 MQ 消息顺序性是得区分全局顺序还是局部顺序的。

如果是全局顺序,那这个需求肯定是不合理的。我们一般做的就是局部顺序。

那局部顺序因为一个 topic 下面有多个 queue,正常发送是肯定无法保证局部顺序的。所以,我们就需要给需要保证顺序的消息分组路由在同一个 queue 里面,这样就能保证消息的顺序性。

但是,如果说消费者是通过线程池并行访问的,这时又会出现问题。那消费者就可以维护内存中的阻塞队列,同样的把同一个订单的消息放在同一个阻塞队列,一个队列对应一个线程并行的处理。

那么问题又来了,顺序消费的 topic 里面的分区数和队列数需要是固定的,不能轻易扩容和缩容。顺序消费的 topic 分区数必须固定,一旦扩容和缩容,消息就可能被路由到其他的队列中或者其他的分区。扩容或者某个分区挂了,会触发 rebalance,那某个分区可能从 A 变 B,就会导致拉错消息,这也会导致乱序问题。

最后,消息消费失败了,会放到队列里重试。那这时候就涉及到两种情况:放到阻塞队列里,这直接就无法保证顺序性了;我们只能放在本地队列。但是放在本地队列,本地重试遇到 rebalance 的时候,就可能会被其他消费者处理,一样会出问题。

所以,MQ 本身就是一个高性能不太可靠的中间件,不应该完全依赖 MQ 去保障。极端情况下很有可能会出问题的,只能做好兜底措施,比如通过定时任务做好数据对账、数据补偿等等。

如何保证消息可靠性?消息重试应该本地重试还是重试队列#

首先,消息丢失的原因主要有几种:1. 从生产者发送到 MQ 的过程中丢失;2. MQ 自己也会弄丢,比如说宕机重启等等的;3. MQ 把消息发送给消费者也可能会丢;4. 消费者处理完消息没有成功确认或者说线程重启了、崩溃了等等。

这些都会导致消息丢失。

首先,从生产者发送到 MQ 的过程中丢失,我们可以用 ACK 机制,MQ 收到直接给生产者一个 ACK 就可以了。

其次,MQ 自己丢失消息,这个我们可以通过开启消息持久化来解决。这样就算宕机重启也不会丢失消息,MQ 集群保证高可用,避免宕机。

最后,消费者还没消费就丢了,那就用消费者 ACK 机制。

如果说消费者消费失败了,这个时候就需要重试机制了。应该放在重试队列肯定是最好的,因为本地队列遇到 rebalance 会丢失消息。消费者数量变化了或者说 Topic 分区数变化了,都会导致 rebalance。本地重试就是在内存里,当队列被回收的时候,发生 rebalance,这些没有被确认和待处理的消息就和队列一起被丢弃了。所以说,本地重试是不靠谱的。

而且,重试队列还有其他的好处,因为它是独立的队列或者 topic,可以去做好监控、观测,重试的次数、延迟、堆积都能监控到,这样就能及时发现问题,进行补偿。

其次,在做好消息的幂等,加上消息的唯一 ID,再加上 Redis 的 setNX 去保证幂等,或者其他方式都是可以的。总之,就是避免消息丢失带来的重复消费问题。

但是,无论再怎么做,MQ 都可能出问题,就是可能因为 rebalance 加上本地重试造成消息丢失,就是可能因为幂等性没做好出问题,也可能因为服务抖动、业务异常等等出现一些现象。

所以,对于重要场景就是要做好兜底的措施。首先,就是死信队列,去保存拒绝、处理失败、超时的这些信息,便于后面复核、修复、重放,配合链路追踪去定位这些问题。

做好定时的消息对账,保证最终消息一致性,保证各种数据最终态没问题,这样才能真正的保证消息的可靠、消息的幂等。

缓存击穿、穿透、雪崩怎么解决#

缓存击穿#

就是一个高并发的 key 过期失效了,这时候大量请求直接打到数据库上,造成数据库压力过大,甚至宕机。

那缓存击穿有很多方案,甚至有很多公司搞了一个中间件或者框架去解决这个问题。主要就是有几种方法。

首先,就是互斥锁,也就是加锁,只有第一个请求去加载数据,其他请求等待。这样就能避免大量请求打到数据库上,但是这种方式会导致大量请求阻塞等待,影响响应时间。

然后,就可以用逻辑过期,设计缓存是永不过期的,数据内部去维护一个过期时间。请求来了之后拿到数据,判断这个过期时间是否过期,如果过期了,那就异步去更新缓存,其他的请求先返回旧数据。这样就能避免大量请求打到数据库上,但是这种方法数据一致性会差一些。

最后,就是一些中间件或者框架,比如快手的 RPC 服务,叫做 cache setter,通过一致性哈希让查同一个 key 的请求都路由到同一个 cache setter。出现了 miss 的情况,那就尝试从 map 里拿出一个 countDownLatch,拿不到就说明是第一个请求,那就给 map 中 put 一个 countDownLatch,然后去更新缓存。后续的请求过来就从 map 里拿到这个 countDownLatch,然后被 await 阻塞住,等待第一个请求更新完缓存之后,调用 countDown() 方法,其他线程就解除阻塞了。

还有京东的方案 jd hot key,通过检测 hot key,直接放在本地缓存里,这样就能避免击穿。

缓存穿透#

就是请求一个根本不存在的数据,这个请求会直接打到数据库上,造成数据库压力过大。

这时候,主要就是对这些请求做好校验、拦截、限流、设置黑名单等等,尽量就在网关这一步拦住非法请求。

然后,就是缓存空值,对于不存在的数据,也缓存一个空值。这样下次请求来的时候,就不会打到数据库上了。

但是,每一次查询的值都不一样,这时候也会造成压力。所以,就上布隆过滤器,写数据库的时候同时写布隆过滤器,这样查询的时候先查布隆过滤器,如果不存在,直接拦截掉,不用查数据库。

但缺点还是有,一个是误判率,另外一个是布隆过滤器本身不支持删除。如果数据库做了删除操作,布隆过滤器还是存在的,这时候就需要定期重建布隆过滤器。

缓存雪崩#

就是缓存集中过期失效了,或者干脆 Redis 宕机了,这时候大量请求打到数据库上,造成数据库压力过大。

第一种方案,就是在固定的过期时间上,加上一个随机值,这样就能避免大量 key 在同一时间过期。

其次,就是针对几乎不太变化的数据,设置永不过期。这样就能避免这些数据过期之后,大量请求打到数据库上。如果说需要修改,那异步的去删除和更新缓存就可以了。

第三种,就是针对能够预知到的热点数据,进行预热。比如说双十一活动之前,把一些热点数据提前加载到缓存里,这样就能避免活动开始之后,大量请求打到数据库上。

最后,就是比如说 Redis 宕机了,内存不够了,或者说缓存里大量冷数据进来导致缓存被淘汰了,或者就是完全不可抗力因素,机房被水淹了导致的缓存雪崩。那就是做好 Redis 的高可用,该上主从上主从,该上集群上集群,多机房的容灾,同城双活,异地多活,该上上。对于不可控的意外情况做好兜底措施,做好服务降级、限流、熔断,避免服务被打垮了。

Redis 的数据持久化#

Redis 提供了两种持久化方式:RDB(Redis DataBase)和 AOF(Append Only File)。

  • RDB 持久化:RDB 是 Redis 默认的持久化方式。它会在指定的时间间隔内生成数据快照,并将其保存到磁盘上的 RDB 文件中。RDB 持久化的优点是恢复速度快,占用空间小,但缺点是可能会丢失最近一段时间的数据,因为它是基于时间间隔进行持久化的。
  • AOF 持久化:AOF 是另一种持久化方式,它会将每个写操作追加到一个日志文件中。AOF 持久化的优点是数据恢复更完整,可以配置为每次写操作都进行持久化,但缺点是文件体积较大,恢复速度较慢。

Redis 还支持混合持久化模式,即同时使用 RDB 和 AOF。在 AOF 重写的时候,把当前内存里的数据以 RDB 的方式写进去,把后面的新命令以 AOF 的形式写下来。这样前半段是 RDB 的二进制,后面是 AOF 的命令,可以在保证数据完整性的同时,提高恢复速度。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

总结一些八股答案
https://www.lansganbs.cn/posts/面试相关/总结一些八股答案/
作者
Zowely
发布于
2026-01-04
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zowely
红叶最多情,一舞寄相思。
公告
欢迎来到我的博客!这里分享计算机等相关内容。