Day16
1. Java为什么要用线程池?
在 Java 中频繁地创建和销毁线程会消耗大量系统资源,影响系统性能。线程池通过复用已创建的线程,避免了反复创建销毁的开销,并且可以控制线程的最大并发数,防止系统被大量线程压垮。同时,线程池还提供了任务队列、线程管理、超时处理等机制,使得线程的使用更加灵活和安全,是构建高性能并发程序的关键组件。
2. 介绍下有哪些线程池?
在 Java 中,线程池是由 Executor 框架提供支持的,核心接口是 ExecutorService,具体实现类由 ThreadPoolExecutor 提供。常用的线程池主要有以下几种,官方提供了几种快捷工厂方法(在 Executors 类中):
newFixedThreadPool(int nThreads)
固定线程池:创建一个固定大小的线程池,核心线程数和最大线程数都为 nThreads,不会回收线程。适用于任务数量恒定、长期执行,避免线程频繁创建销毁。newCachedThreadPool()
缓存线程池:线程数不固定,有空闲线程就复用,否则就创建新线程;空闲线程超过 60 秒就回收。适用于执行很多短期异步任务,任务量不确定,适合高并发、瞬时请求。newSingleThreadExecutor()
单线程线程池:始终只有一个线程执行任务,保证任务串行执行、按顺序处理。适用于需要按顺序执行任务、避免并发访问的场景(如日志记录、按顺序发送短信等)。newScheduledThreadPool(int corePoolSize)
定时/周期任务线程池:支持任务的延时执行和周期执行。适用于周期性任务调度,如心跳检测、定时任务等。ThreadPoolExecutor
可自定义:所有上述线程池本质上都是对 ThreadPoolExecutor 的封装。
3. Redis内存淘汰策略有哪些?
Redis 内存淘汰策略是指当内存使用达到设置的最大上限时,Redis 需要决定哪些数据要被删除,以释放空间存储新数据。
当 Redis 设置了 maxmemory(最大可用内存)之后,可以通过 maxmemory-policy 来配置淘汰策略。主要有以下几类:
- noeviction:默认策略,不淘汰,只报错:写操作会返回错误。
- allkeys-lru:从所有键中,优先淘汰最近最少使用的键。
- volatile-lru:从设置了过期时间(expire)的键中,淘汰最近最少使用的键。
- allkeys-random:从所有键中随机淘汰一个键。
- volatile-random:从有过期时间的键中随机淘汰一个键。
- volatile-ttl:从设置了过期时间的键中,淘汰即将过期的键(TTL最小)。
- allkeys-lfu:从所有键中,淘汰最不常用的键。
- volatile-lfu:从设置了过期时间的键中,淘汰最不常用的键。
redis.conf 文件配置方式:
# 设置最大内存限制
config set maxmemory 100mb# 设置淘汰策略,比如使用 allkeys-lru
config set maxmemory-policy allkeys-lru
4. Redis过期删除策略是什么?
Redis 采用惰性删除和定期删除相结合的方式来处理过期键,以在性能和内存效率之间取得平衡。惰性删除指的是只有在访问某个 key 时,才会检查其是否过期,若过期则立即删除,这种方式 CPU 开销极低,但可能导致大量过期但未被访问的数据长期占用内存。为了解决这一问题,Redis 还引入了定期删除机制,每隔一段时间会随机抽取一部分设置了过期时间的 key 进行检查和删除,这样可以在不影响主线程性能的前提下清除部分无效数据。通过将这两种策略结合使用,Redis 能够有效控制 CPU 占用的同时避免内存被过期数据占满,实现系统性能和资源利用率的双重优化。
5. Redis是怎么实现惰性删除的?
Redis 的 惰性删除策略(Lazy Deletion) 是通过
expireIfNeeded(redisDb *db, robj *key)
函数实现的,这个函数定义在 db.c 文件中。
- 判断是否过期:首先通过
keyIsExpired(db, key)
判断该 key 是否已经过期。- 如果未过期:直接返回,Redis 会继续正常执行后续操作。
- 如果已过期:
- Redis 会执行删除操作
- 是否采用异步删除(不会阻塞主线程),取决于配置项
lazyfree-lazy-expire
(默认是关闭的)
return server.lazyfree_lazy_expire ? dbAsyncDelete(db, key) : dbSyncDelete(db, key);
- 如果是异步删除,Redis 会将该删除操作交给后台线程去处理
- 最终 Redis 返回 NULL 给客户端,表示该 key 已过期并被移除
- 触发时机:只要客户端访问 key(如 GET、SET、DEL 等),都会先隐式调用
expireIfNeeded()
检查是否过期。
6. Redis是怎么实现定期删除的?
Redis 的定期删除通过后台任务每隔一段时间随机抽取部分数据库中的 key,检查它们是否过期,如果过期就删除,从而避免大量过期但未被访问的 key 长期占用内存。
Redis 是通过activeExpireCycle()
函数(位于 expire.c 文件)来实现定期删除。该函数会被 Redis 的主循环周期性调用:
- 触发机制:
- 每 100 ms 左右执行一次(在 Redis 的事件循环中定时触发)。
- 每次从数据库中抽样一部分设置了过期时间的 key(默认每次最多 20 个),进行过期检查。
- 检查和删除:
- 对这些 key 调用
keyIsExpired()
判断是否过期。- 如果过期,则直接删除(会调用和惰性删除一样的
expireIfNeeded()
)。- 如果这次扫描中超过 25% 的 key 是过期的,那么认为当前数据库中过期 key 比例高,于是继续扫描(进行多轮采样,直到命中率小于 25% 或事件耗尽)。
- 事件限制:
- Redis 会控制这次循环的总执行时间,防止长时间占用 CPU(默认上限 1ms 左右)。
- 因此这个删除是渐进式、有限度的,不会一口气清掉所有过期数据。
7. RocketMQ怎么处理分布式事务?
RocketMQ 通过三阶段的事务消息机制(发送预备消息 → 执行业务逻辑并提交本地事务 → 消息确认提交或回滚),实现分布式事务的一致性控制,避免数据不一致问题。
- 发送半消息(prepare message):
- 生产者先向 MQ 发送一条“半消息”(消息状态为暂时不可投递),此时消息会存入消息服务器,但消费者不可见。
- 消息处于“待确认”状态。
- 执行本地事务:
- 消息发送成功后,生产者执行本地事务(如修改数据库、写账本等)。
- 本地事务执行完后,生产者向 RocketMQ 提交事务状态:
- 提交(COMMIT):MQ 向半消息转为正常消息,投递给消费者
- 回滚(ROLLBACK):MQ 删除半消息,不投递
- 未知(UNKNOWN):MQ 无法判断事务结果
- 事务回查(Check):
- 如果 MQ 长时间未收到事务状态反馈(如生产者宕机),Broker 会主动发起事务状态回查。
- 生产者需实现
TransactionListener
接口中的checkLocalTransaction()
方法,返回事务状态,供 MQ 决定是否提交消息。
在 RocketMQ 中,分布式事务通过「半消息机制」实现。首先,A 服务(消息生产者)向 Broker 发送一条 Half Message(即暂不可投递的“半消息”),这条消息中通常包含 B 服务(消费者)将要执行的操作信息,比如“账户增加 100 元”。Broker 成功接收该消息后,会将其暂时保存,但不会投递给消费者。随后,A 服务执行本地事务逻辑,比如向数据库写入订单记录。如果事务执行成功,A 服务会向 Broker 发送 Commit 消息,Broker 此时才会将该消息投递给 B 服务;若本地事务失败,则发送 Rollback 消息,Broker 会删除该半消息,不再投递。若由于网络等原因,Broker 一直未收到事务状态确认(即 Commit 或 Rollback),RocketMQ 会定时通过 事务回查机制 向 A 服务发起回调请求,确认事务的真实执行结果,最终决定是否提交或回滚消息。
8. RocketMQ消息顺序怎么保证?
在 RocketMQ 中,保证消息顺序主要依赖于 顺序消息(Ordered Message)机制。
RocketMQ 保证消息顺序的关键在于消息发送和消费始终路由到同一个队列(MessageQueue)。在发送顺序消息时,Producer 会通过自定义的 MessageQueueSelector 选择逻辑,根据某个业务关键字段(如订单 ID、用户 ID等)将同一类消息始终发送到同一个队列,从而在该队列内保持顺序。Consumer 在消费时开启顺序消费模式(顺序消费是单线程的),确保消息被严格暗战存储顺序依次消费。因此,RocketMQ 是通过“同类消息进同一队列 + 单线程消费”来实现局部顺序性。需要注意的是,这种顺序性仅在单个队列内有效,也就是分区有序,无法做到全局顺序。
比如一个订单系统中,同一个订单会产生多个状态变更消息(下单、支付、发货),为了保证这些状态变化按顺序消费,可以用订单号作为 hash key,将这些消息都发送到同一个队列。消费端则按队列顺序处理,就可以确保消息顺序一致。
9. RocketMQ消息积压了怎么办?
当 RokcetMQ 出现消息积压时,首先应排查是否是 Consumer 消费能力不足或消费异常导致的,比如消费失败重试、线程数不足、业务逻辑慢等。其次,需要监控 Broker 中的消息堆积情况(如堆积条数、消息进度落后),定位是哪个 Consumer Group 出问题。之后,可以通过增加消费者实例数、提升消费线程数、优化消费逻辑或临时开启批量消费等方式进行缓解。如果仍无法解决,考虑削峰填谷:对生产端限流、或将消息持久化后异步处理。极端情况下,可以考虑使用临时的 消费补偿机制 来快速清理堆积。
怎么排查消息堆积严重: 先通过 RocketMQ 控制台或 mqadmin 查看具体哪个 Consumer Group 积压严重,再检查 Consumer 是否存活、消费线程是否足够、是否存在消费失败或阻塞,然后根据具体情况进行水平扩容、优化消费逻辑、开启批量消费,最后评估是否需要限流生产者或引入补偿机制来兜底。