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)人工处理。
- 生产者端:避免重复发送
从消息源头减少重复,需确保 "生产者不重复发消息":
- 幂等性发送:给每条消息生成唯一
msgId,生产者发送前先检查本地缓存 / RocketMQ 的 "事务消息" 机制,避免重复发送; - 确认机制:依赖 MQ 的生产者确认(如 RabbitMQ 的
publisher confirms、Kafka 的acks=all),确保 Broker 成功接收后再结束,避免因 "未确认" 触发重试。
- Broker 端:去重缓存与重试管控
部分 MQ 可通过 Broker 层配置减少重复投递:
- 去重缓存:Broker 维护已处理消息的
msgId缓存(如 Redis),接收消息时先校验,重复则直接丢弃(需注意缓存清理策略,避免内存溢出); - 限制重试次数:配置最大重试次数(如 RabbitMQ 的
x-max-retry-count),超过次数后移至死信队列,避免无限重复投递; - Kafka 特殊处理:利用
offset机制,消费者维护已消费的offset并及时提交(建议 "业务成功后提交 offset"),重启后从最新offset消费,减少重复。
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. 有误判、无漏判 | - 误判:"不存在"→"存在"(假阳性),无法完全避免,只能通过调大 m 或 k 降低;- 无漏判:"存在"→"不存在"(假阴性)绝对不会发生,只要元素曾插入,必能判定 "可能存在"。 |
| 4. 不支持删除 | 因为一个 bit 位可能被多个元素共享(如索引 3 被 "apple" 和 "grape" 同时映射),删除时将 bit 置 0 会导致其他元素的查询误判(假阴性)。(注:有变种 "计数布隆过滤器" 支持删除,但空间占用翻倍,实用性较低) |
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)。