八股-2025.10.24

1. 消息队列如何保证不重复消费?

解决重复消费的核心是 **"让消费逻辑具备幂等性"**(多次执行结果一致),同时配合 MQ 机制减少重复投递,具体分 4 类实战方案:

A. 业务层幂等设计:最通用、最核心的方案

无论 MQ 如何管控,最终需通过业务逻辑确保 "重复消费无害",常见 3 种实现方式:

a.基于 "业务唯一 ID" 去重给每条消息绑定一个全局唯一 ID(如订单号、交易流水号,或用雪花算法生成msgId),消费前先校验该 ID 是否已处理:

  • 高并发场景:用 Redis 的SETNX(或setIfAbsent)判断,例如key="order:pay:"+orderNo,存在则跳过,不存在则执行业务并设置过期时间(避免内存占用);
  • 强一致性场景(如金融):用数据库防重表,表中msg_unique_id设唯一索引,消费前插入记录,插入成功则处理,失败则跳过(利用数据库唯一约束保证幂等)。

b.基于状态机控制针对有明确状态流转的业务(如订单:待支付→已支付→已发货),消费前先检查当前状态是否符合处理条件。例如 "订单支付成功" 消息,若订单已处于 "已支付" 状态,直接跳过重复处理。

b.基于乐观锁更新数据库操作时,用版本号 / 时间戳控制并发,避免重复更新。例如扣库存时,SQL 写为UPDATE stock SET num=num-1, version=version+1 WHERE goods_id=? AND version=?,只有版本匹配时才执行,重复消费会因版本不匹配失败。

B. MQ 手动确认机制:减少重复投递源头

针对支持手动 Ack 的 MQ(如 RabbitMQ),关键是 **"业务成功后再发 Ack"**,而非拿到消息就确认:

  • 禁用自动 Ack,开启手动 Ack(RabbitMQ 需配置ackMode=MANUAL);
  • 消费者流程:先接收消息→执行业务逻辑→业务成功后,调用channel.basicAck()发送确认→Broker 删除消息;
  • 若业务失败(如数据库临时不可用),可拒绝消息(basicReject)并让其进入重试队列,避免直接丢弃;若业务无法重试(如参数错误),直接 Ack 并丢入死信队列(DLQ)人工处理。
  1. 生产者端:避免重复发送

从消息源头减少重复,需确保 "生产者不重复发消息":

  • 幂等性发送:给每条消息生成唯一msgId,生产者发送前先检查本地缓存 / RocketMQ 的 "事务消息" 机制,避免重复发送;
  • 确认机制:依赖 MQ 的生产者确认(如 RabbitMQ 的publisher confirms、Kafka 的acks=all),确保 Broker 成功接收后再结束,避免因 "未确认" 触发重试。
  1. Broker 端:去重缓存与重试管控

部分 MQ 可通过 Broker 层配置减少重复投递:

  • 去重缓存:Broker 维护已处理消息的msgId缓存(如 Redis),接收消息时先校验,重复则直接丢弃(需注意缓存清理策略,避免内存溢出);
  • 限制重试次数:配置最大重试次数(如 RabbitMQ 的x-max-retry-count),超过次数后移至死信队列,避免无限重复投递;
  • Kafka 特殊处理:利用offset机制,消费者维护已消费的offset并及时提交(建议 "业务成功后提交 offset"),重启后从最新offset消费,减少重复。

https://www.bilibili.com/video/BV1sc411t7ny/?spm_id_from=333.337.search-card.all.click&vd_source=3abe3667e67749032f72d6f512b2a967

2. 布隆过滤器的原理。

布隆过滤器是一种空间效率高,适合大规模数据集合的概率型数据结构,主要用于判断一个数据在不在集合中。布隆过滤器包括一个位数组和多个哈希函数,初始时所有位置都被置为0。当一个元素经过多个哈希函数计算后,将对应的位数组位置置为1.查询时,如果元素经过哈希函数计算后的位置都是1,则说明元素可能存在,如果有一个位置是0,则元素一定不存在。

|--------------|-------------------------------------------------------------------------------------------------------------------------|
| 特性 | 说明 |
| 1. 空间高效 | 用 bit 存储,空间复杂度是 O(m)m 远小于集合实际大小)。例如存储 1 亿个元素,m 取 1.2GB 时,误判率可低至 0.1%,远优于哈希表(哈希表存储 1 亿个整数需约 400MB,但无误判,需对比)。 |
| 2. 查询 / 插入高效 | 时间复杂度是 O(k) (仅需执行 k 次哈希和取模),k 通常取 5~10,速度远超数据库查询或磁盘 IO。 |
| 3. 有误判、无漏判 | - 误判:"不存在"→"存在"(假阳性),无法完全避免,只能通过调大 mk 降低;- 无漏判:"存在"→"不存在"(假阴性)绝对不会发生,只要元素曾插入,必能判定 "可能存在"。 |
| 4. 不支持删除 | 因为一个 bit 位可能被多个元素共享(如索引 3 被 "apple" 和 "grape" 同时映射),删除时将 bit 置 0 会导致其他元素的查询误判(假阴性)。(注:有变种 "计数布隆过滤器" 支持删除,但空间占用翻倍,实用性较低) |

https://www.bilibili.com/video/BV1zK4y1h7pA/?spm_id_from=333.337.search-card.all.click&vd_source=3abe3667e67749032f72d6f512b2a967

3.说说synchronize的用法及原理

A.用法

可作用于方法和代码块。

a.作用于实例方法

锁对象:当前类的实例对象(this),即"哪个对象调用该方法,就锁哪个对象"。

效果:同一时间,只有一个线程能执行该实例的此方法,不同实例的方法可以并行执行(因为锁对象不同)。

b.作用于静态方法

锁对象:当前类的Class对象(每个类在JVM中只有一个Class对象,全局唯一)。

效果:同一时间,所有线程(无论哪个实例)执行该静态方法都需要排队,锁粒度是"整个类"。

c.作用于同步代码块

锁对象:手动指定的"任意对象"(需要是引用类型,如this,Class对象,自定义Object实例),锁粒度可精准控制)(只锁临界区,不锁整个方法)。

B.底层原理

依赖JVM的"对象头"和"监视器",需结合"Java对象结构"和"字节码指令"理解:

a.核心依赖:

Java每个对象在内存中分为三个部分:对象头,实例数据,对齐填充,对象头是synchronized实现锁的关键。

关键逻辑:synchronized的锁状态会直接修改对象头的Mark Down-当线程获取锁时,JVM会根据当前锁的状态(无锁->偏向->轻量->重量)更新Mark Down的标志位,实现锁的升级。

b.锁的核心

synchronized的"互斥"能力"依赖Monitor(监视器),可理解为一个"由JVM管理的,负责线程竞争的工具",其本质是一个C++实现的对象(ObjectMonitor)。

d.Monitor 的工作流程(以同步代码块为例):

线程进入synchronized代码块时,会尝试获取 Monitor 的 Owner 权限:

  • 若 Monitor 无 Owner(初始状态),当前线程直接成为 Owner,锁状态从 "无锁" 升级(如偏向锁);
  • 若 Monitor 已有 Owner(锁被占用),当前线程进入 EntryList 阻塞,等待 Owner 释放锁;
  • 线程执行完同步代码块,或调用wait()方法时:
  • 若执行完代码:释放 Monitor(Owner 置空),唤醒 EntryList 中等待的线程,重新竞争锁;
  • 若调用wait():线程从 Owner 移至 WaitSet,释放锁;待其他线程调用notify(),再从 WaitSet 移回 EntryList,重新竞争锁。

C. 字节码层面的体现(加分项)

a.通过javac编译synchronized代码后,字节码中会出现两个关键指令,对应 "获取锁" 和 "释放锁":

b.monitorenter:进入同步代码块时执行,尝试获取 Monitor 的 Owner 权限(若失败则阻塞);

c.monitorexit:退出同步代码块时执行,释放 Monitor 的 Owner 权限(唤醒等待线程)。
https://www.bilibili.com/video/BV1aG4recEQw/?spm_id_from=333.337.search-card.all.click&vd_source=3abe3667e67749032f72d6f512b2a967

https://www.bilibili.com/video/BV14C4y127W1/?spm_id_from=333.337.search-card.all.click

4. 线程间同步方式有哪些?

A.互斥锁:保证同一时间只有一个线程访问共享资源。

B.自旋锁:一种非阻塞锁,线程会尝试循环获取锁,适用于持有锁时间极短的情况下。

C.读写锁:基于"读写分离"的优化锁,分为读锁(共享锁,多个线程可同时读)和写锁(排他锁,进一个线程可写),解决"读多写少"场景下的性能问题。

D.信号量:通过"许可证"控制同时访问资源的线程数量,可理解为"多线程的限流工具"。

E.条件变量:配合ReentranLock使用,实现"精细化的线程等待/唤醒",可替代Object的wait()/notify(),支持多个等待队列(一个锁可对应多个Condition)。

https://www.bilibili.com/video/BV1oQ4y1C73G/?spm_id_from=333.337.search-card.all.click&vd_source=3abe3667e67749032f72d6f512b2a967

相关推荐
我是华为OD~HR~栗栗呀4 小时前
华为OD-Java面经-21届考研
java·c++·后端·python·华为od·华为·面试
烟袅5 小时前
告别 var!深入理解 JavaScript 中 var、let 和 const 的差异与最佳实践
javascript·面试
初级程序员Kyle8 小时前
开始改变第一天 JVM的原理到调优(2)
java·面试
绝无仅有8 小时前
京东面试题解析:同步方法、线程池、Spring、Dubbo、消息队列、Redis等
后端·面试·github
绝无仅有9 小时前
京东面试题解析:String与StringBuilder的区别、装箱拆箱、重载与重写总结
后端·面试·github
牛客企业服务20 小时前
企业招聘新趋势:「AI面试」如何破解在线作弊难题?
人工智能·面试·职场和发展·招聘·ai招聘
拉不动的猪20 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
怪兽201421 小时前
Android View, SurfaceView, GLSurfaceView 的区别
android·面试
xxxxxxllllllshi1 天前
【Elasticsearch查询DSL API完全指南:从入门到精通】
java·大数据·elasticsearch·搜索引擎·面试·全文检索·jenkins