总结一些八股答案
怎么保证 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 的命令,可以在保证数据完整性的同时,提高恢复速度。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


