面试专题
XXL-JOB 的面试重点不是背“分布式调度平台”,而是能解释它如何在不引入 Redis/ZooKeeper 的前提下,完成调度互斥、执行器发现、任务串行、失败重试和结果收敛。
先给答案:数据库锁只负责分配调度状态
Section titled “先给答案:数据库锁只负责分配调度状态”XXL-JOB 用数据库行锁串行化“谁来推进下一次触发时间”,锁内完成扫描和状态更新,锁外再由时间轮、触发线程池和执行器完成真正执行。调度、排队、执行、回调因此是几个相互衔接而非一次提交完成的阶段。
这意味着系统提供的是可恢复的最终状态机,不是端到端恰好一次:网络超时可能造成触发结果不确定,执行器队列和阻塞策略决定并发边界,回调失败还要靠重试收敛。业务任务仍需以幂等和可重入承接这些边界。
XXL-JOB 如何避免多个 admin 重复调度?
Section titled “XXL-JOB 如何避免多个 admin 重复调度?”30 秒版:admin 每轮调度先对 xxl_job_lock 的唯一行执行 SELECT ... FOR UPDATE,在同一事务里扫描任务、计算下次时间并批量更新。只有持锁节点能决定本轮任务的下一个时间点,HTTP 触发在释放锁后异步执行。
展开:入口是 JobScheduleHelper#scheduleThread(xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobScheduleHelper.java:79-171),锁 SQL 在 XxlJobLockMapper.xml:6-10。事务提交后才释放锁,因此其他节点读到的是已经推进过的 trigger_next_time。
加分项:它不是 Quartz 式“抢到锁的节点执行全部任务”,而是锁内只做触发时刻分配,真正请求由时间轮线程和触发线程池完成。
常见错答:说 XXL-JOB 用 Redis 分布式锁,或者说锁一直持有到任务执行完成。
为什么需要时间轮?
Section titled “为什么需要时间轮?”30 秒版:调度线程只预读未来 5 秒的任务,未来任务推入 60 个秒槽;ring 线程每秒取当前和前两个刻度,再把 jobId 交给触发池。这样 DB 扫描和 HTTP 请求解耦。
展开:预读和三分支在 JobScheduleHelper.java:85-141,时间轮消费在 :219-231。锁内只操作内存结构,避免网络超时把 DB 行锁占住。
加分项:它不需要 remainingRounds,因为预读窗口 5 秒远小于 60 秒一圈;这是利用问题域约束换来的简化。
常见错答:把 XXL-JOB 的时间轮说成 Netty 的通用多圈时间轮。
执行器如何保证同一个任务串行?
Section titled “执行器如何保证同一个任务串行?”30 秒版:执行器按 jobId 保存一个 JobThread,该线程拥有一个 LinkedBlockingQueue<TriggerRequest>,所有同 job 触发先入队再串行消费。
展开:队列和去重集合在 JobThread.java:29-43,入队在 :54-70,消费在 :105-162。isRunningOrHasQueue 同时看运行状态和队列积压,供 idleBeat 判断。
加分项:线程空闲 30 个 3 秒轮次后会自毁(JobThread.java:176-185),所以不是无限制地为每个历史 job 保留线程。
常见错答:说执行器用一个全局线程池保证所有任务全局串行。
三种阻塞策略有什么区别?
Section titled “三种阻塞策略有什么区别?”30 秒版:SERIAL_EXECUTION 继续排队,DISCARD_LATER 忙时直接失败,COVER_EARLY 停掉旧 JobThread 后让新触发接管。
展开:策略枚举在 ExecutorBlockStrategyEnum.java:6-29,判断在 ExecutorBizImpl.java:132-158。覆盖旧线程依赖 toStop 和 interrupt,并不保证正在执行的业务代码立即停止。
加分项:默认策略由 admin 的 JobTrigger#processTrigger 设为 SERIAL_EXECUTION(xxl-job-admin/.../JobTrigger.java:143)。
常见错答:把 COVER_EARLY 解释为操作系统级强杀线程。
执行器注册和判死怎么做?
Section titled “执行器注册和判死怎么做?”30 秒版:执行器每 30 秒向任一可达 admin 注册,admin 用唯一键 upsert 更新 update_time;监控线程每 30 秒删除超过 90 秒未更新的地址,再重建执行器地址缓存。
展开:心跳在 ExecutorRegistryThreadHelper.java:48-69,upsert 在 JobRegistryHelper.java:174-194,判死和聚合在 :72-141。
加分项:注册成功后不是立即刷新 xxl_job_group.address_list,freshGroupRegistryInfo 留为空方法,扩容节点最长等一个监控周期才接流量。
常见错答:说 admin 节点之间通过内存同步注册信息。
回调失败会不会丢结果?
Section titled “回调失败会不会丢结果?”30 秒版:正常回调先进入批量消息队列,最多 50 条;所有 admin 地址失败后落盘到 callbacklogs,重试线程每 30 秒读取并再次发送。admin 收到后按 handleCode > 0 拒绝重复完成。
展开:批量队列和重试线程在 TriggerCallbackThreadHelper.java:63-120,失败落盘在 :153-237,重复回调保护在 JobCompleteHelper.java:127-136。
加分项:文件重试采用“先读后删、再回调”,删除与发送之间存在崩溃窗口,因此它是工程上的最终一致兜底,不是事务消息。
常见错答:把它说成严格 exactly-once。
为什么 ROUND 在 admin 集群下会退化?
Section titled “为什么 ROUND 在 admin 集群下会退化?”ExecutorRouteRound 的计数器保存在单个 admin 进程内存中(xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/route/strategy/ExecutorRouteRound.java:19-37)。多个 admin 节点各自维护计数,轮询序列会分叉;需要集群确定性时应选无状态的一致性哈希,或接受 BUSYOVER 的探测开销。
FAILOVER 和 BUSYOVER 的区别是什么?
Section titled “FAILOVER 和 BUSYOVER 的区别是什么?”FAILOVER 逐个调用 beat(),只验证进程和端口可达(ExecutorRouteFailover.java:24-47;ExecutorBizImpl.java:28-30)。BUSYOVER 调用 idleBeat(jobId),验证该 job 在目标节点没有运行且队列为空(ExecutorRouteBusyover.java:24-46;ExecutorBizImpl.java:33-44)。后者不是机器级 CPU 负载均衡。
失败重试为什么不重复广播?
Section titled “失败重试为什么不重复广播?”首次广播在 JobTrigger.java:99-110 对每个地址生成 index/total;失败监控把原 executorShardingParam 传回 JobTriggerPoolHelper#trigger(JobFailAlarmMonitorHelper.java:50-56)。因此重试走单分片路径,而不是再次满足 shardingParam == null 的广播条件。
任务超时后业务一定停止吗?
Section titled “任务超时后业务一定停止吗?”不一定。JobThread 用 FutureTask#get(timeout) 等待,超时后仅 futureThread.interrupt()(JobThread.java:131-158)。业务代码可能忽略中断继续运行,所以 handler 必须可取消、幂等,外部资源也要有超时。
admin 如何处理“执行器已执行但回调丢失”?
Section titled “admin 如何处理“执行器已执行但回调丢失”?”JobCompleteHelper 每 60 秒查找超过 10 分钟仍未完成的日志(xxl-job-admin/src/main/java/com/xxl/job/admin/business/scheduler/thread/JobCompleteHelper.java:59-81)。若执行器已不在线,就主动把日志标为失败并走 JobCompleter#complete,避免永久停留在运行中。
线上出现大量 trigger timeout,怎么排查?
Section titled “线上出现大量 trigger timeout,怎么排查?”先区分 admin 到 executor 的触发超时与 handler 执行超时:查看 trigger_code、执行器地址和慢池计数;再检查 FAILOVER/BUSYOVER 是否串行探测多个不可达节点。最后确认 executor 的 trigger 是否只是入队,是否错误地在 Netty 业务线程执行了阻塞逻辑。
分片任务出现重复扣款,怎么定位?
Section titled “分片任务出现重复扣款,怎么定位?”沿 logId -> executorShardingParam -> broadcastIndex/Total 检查是否是回调重复、失败分片重试,还是任务本身没有按分片参数幂等。admin 侧 handleCode > 0 只能防止重复完成,不能撤销业务副作用;扣款必须使用业务幂等键。
反向问面试官
Section titled “反向问面试官”- 你们的任务是单节点、分片广播,还是需要跨任务依赖?
- handler 是否保证中断可取消和业务幂等?
- admin 与 executor 的日志、回调文件和数据库分别如何监控容量?