news 2026/5/27 18:59:22

NestJS异步任务队列实战:Bull/BullMQ高级配置与性能调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NestJS异步任务队列实战:Bull/BullMQ高级配置与性能调优

1. 项目概述:当异步任务成为“狂牛”

在构建现代化的后端服务时,异步任务处理几乎是每个开发者都会遇到的课题。想象一下,你正在开发一个电商应用,用户下单后,系统需要发送邮件、更新库存、记录日志、推送通知,甚至触发下游的仓储系统。如果把这些操作全部塞进一个同步的API请求里,用户就得盯着浏览器转圈圈,等待十几秒甚至更久,体验极差,而且任何一个环节出错,整个请求就会失败。于是,我们引入了队列(Queue)——一个专门用来处理这些“后台杂活”的中间人。它接收任务,排队,然后由专门的“工人”(Worker)慢慢处理,让主线程可以立刻响应用户:“订单已收到,正在处理中!”

然而,队列用不好,就会从温顺的工具变成一头难以驯服的“狂牛”(Bull Job)。这里的“Bull”一语双关,既指代了Node.js生态中一个非常流行且强大的队列库Bull(以及它的TypeScript版本BullMQ),也形象地比喻了那些失控的队列任务:它们可能因为一个未处理的错误而不断重试,塞满你的Redis内存;可能因为并发设置不当,拖垮你的数据库连接;也可能因为缺乏监控,在深夜悄无声息地堆积如山,最终在凌晨三点把你的服务压垮。成为一名“队列低语者”(Queue Whisperer),意味着你不仅能使用队列,更能洞察其脾性,优雅地驾驭它,使其成为系统稳定可靠的基石,而非噩梦的来源。

NestJS作为一个高度集成的企业级框架,为使用Bull提供了优雅的模块(@nestjs/bull)。但框架的封装在带来便利的同时,有时也掩盖了底层库的复杂性。本项目“The Queue Whisperer”的目标,就是深入NestJS与Bull的结合部,分享一系列实战中总结出的高级模式、配置技巧和避坑经验,帮助你像专业人士一样,驯服那些狂野的异步任务。

2. 核心架构与设计哲学

2.1 为什么是Bull/BullMQ?

在Node.js的世界里,队列库的选择不少,比如KueBee-QueueAgenda(更偏向定时任务)。Bull及其继任者BullMQ能脱颖而出,成为NestJS官方推荐的选择,主要基于以下几点:

  1. Redis驱动,性能与持久化兼得:Bull使用Redis作为存储后端。Redis的内存特性保证了极高的读写速度,同时支持RDB和AOF持久化,确保了任务不会因为进程重启而丢失。这使得Bull既能处理高吞吐量的任务,又能提供可靠性的保证。
  2. 丰富的功能特性:Bull支持延迟任务、优先级队列、重复任务(Cron模式)、任务进度报告、事件监听等。这些功能覆盖了生产环境中绝大部分异步场景的需求。
  3. 健壮的错误处理与重试机制:任务失败后,Bull可以自动根据配置进行重试,并最终将彻底失败的任务移入“失败作业”集合,便于后续排查和手动重试。
  4. 与NestJS的无缝集成@nestjs/bull包提供了装饰器(如@Processor@Process)和依赖注入支持,让队列消费者(Processor)的编写如同编写普通的NestJS Provider一样简单,大大降低了使用门槛。

然而,正是这种“简单”的集成,容易让人忽略其底层复杂性。一个常见的误区是,开发者认为只要用@nestjs/bull装饰一下,队列就能高枕无忧。实际上,Bull的配置项、Redis的连接管理、并发控制、错误传播等,都需要精心设计。

2.2 队列模式设计:超越简单的“生产者-消费者”

在简单的教程里,我们通常看到一个生产者(Producer)向一个队列(Queue)投递任务,一个消费者(Processor)处理它。但在实际生产环境中,我们需要更细致的模式。

2.2.1 队列拓扑结构不要把所有类型的任务都扔进一个叫default的队列。应该根据业务领域和重要性进行拆分。例如:

  • email-queue: 专门处理邮件发送,对实时性要求不高,但需要保证至少送达一次。
  • payment-queue: 处理支付回调、对账,对数据一致性要求极高。
  • report-queue: 生成数据报表,通常是CPU密集型任务,可以设置较低的并发度。
  • notification-queue: 处理App推送、短信,可能依赖第三方服务,需要做好限流和降级。

在NestJS中,这对应着创建不同的Bull模块:

// email.module.ts BullModule.registerQueue({ name: 'email' }); // payment.module.ts BullModule.registerQueue({ name: 'payment' });

这样的拆分好处明显:不同队列可以独立配置(不同的Redis连接、并发数)、独立监控、独立扩缩容。一个队列的阻塞(比如邮件服务商故障导致大量重试)不会影响支付回调的及时处理。

2.2.2 作业(Job)数据结构设计作业的数据(data)是序列化后存储在Redis中的。设计一个清晰、向后兼容的数据结构至关重要。

// 反面教材:随意传递数据 await this.emailQueue.add('send-welcome', { userId: 123, email: 'user@example.com' }); // 推荐做法:定义强类型的作业数据接口 interface WelcomeEmailJobData { userId: number; email: string; locale?: string; // 可选字段,为未来扩展留空间 metadata: { source: 'user-registration' | 'admin-invite'; requestId: string; // 用于全链路追踪 }; }

在Processor端,使用泛型来获取类型提示:

@Process('send-welcome') async handleWelcomeEmail(job: Job<WelcomeEmailJobData>): Promise<void> { const { userId, email, metadata } = job.data; // 现在有了完整的类型安全 }

2.2.3 事件驱动与状态监听Bull的队列和作业会发射大量事件(active,completed,failed,stalled等)。在NestJS中,你可以通过装饰器轻松监听这些事件,实现复杂的业务流程和监控。

@OnQueueEvent('failed') onFailed(jobId: number | string, failedReason: string) { this.logger.error(`Job ${jobId} failed: ${failedReason}`); // 可以触发告警,如发送Slack消息、写入特定监控队列 } @OnQueueEvent('completed') onCompleted(job: Job) { this.metricsService.increment('jobs.completed', 1, { queue: job.queue.name }); }

注意:事件监听器中的逻辑应当轻量、快速,避免执行耗时操作。如果需要基于任务完成触发复杂的后续操作,更好的模式是让任务处理器(Processor)在成功完成后,向另一个队列投递一个新任务,形成工作流(Workflow)。

3. 高级配置与性能调优实战

3.1 Redis连接配置:稳定性是第一要务

Bull的性能和稳定性极度依赖Redis。一个配置不当的Redis连接池可能就是系统的不定时炸弹。

3.1.1 使用连接池并配置合理的超时BullModule.forRootregisterQueue时,不要使用默认配置。

BullModule.forRoot({ redis: { host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, // 关键配置开始 maxRetriesPerRequest: 3, // 每个请求失败重试次数 enableReadyCheck: true, // 启用就绪检查 retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); // 指数退避,最大延迟2秒 return delay; }, // 连接池配置(如果使用ioredis) ...(process.env.NODE_ENV === 'production' && { reconnectOnError: (err) => { // 仅在某些错误下重连 const targetError = 'READONLY'; if (err.message.includes(targetError)) { return true; } return false; }, }), }, })

实操心得:在Kubernetes或Docker Swarm等容器化环境中,Redis可能因为滚动更新或网络抖动暂时不可用。配置retryStrategy和合理的maxRetriesPerRequest能让你的队列服务在短暂中断后自动恢复,而不是直接崩溃。

3.1.2 为不同队列配置独立的Redis连接对于核心支付队列和普通的日志队列,它们的优先级和对稳定性的要求是不同的。你可以为高优先级队列配置一个更独立、资源保障更好的Redis实例或集群。

// 在核心模块中 BullModule.registerQueueAsync({ name: 'payment', useFactory: () => ({ redis: { host: process.env.PAYMENT_REDIS_HOST, // 专用Redis实例 // ... 其他配置,可能开启更多持久化选项 }, defaultJobOptions: { removeOnComplete: 100, // 只保留最近100个成功记录 removeOnFail: 500, // 保留更多失败记录用于排查 attempts: 5, backoff: { type: 'exponential', delay: 2000, // 首次重试2秒后 }, }, }), });

3.2 并发控制与限流:防止“狂牛”冲垮服务

这是驯服“狂牛”最关键的一环。无限制的并发会瞬间耗尽数据库连接、拖垮第三方API、导致内存溢出。

3.2.1 理解limiter配置Bull的队列级limiter配置非常强大,它可以平滑流量,防止突发请求。

BullModule.registerQueue({ name: 'third-party-api', limiter: { max: 100, // 单位时间内最大执行数 duration: 60000, // 时间窗口,单位毫秒(这里是1分钟) bounceBack: false, // 设为true时,超限任务会被延迟而非拒绝 }, defaultJobOptions: { attempts: 3, }, });

这个配置意味着,third-party-api队列每分钟最多处理100个作业。这对于调用有严格QPS限制的外部API(如发送短信的服务商)是救星。

3.2.2 进程级与Worker级并发

  • 进程级并发:在NestJS中,一个@Processor()装饰的类就是一个单独的进程(如果使用多个进程,则需要配合cluster模块)。这个进程内,所有@Process()装饰的方法共享一个Node.js事件循环。
  • Worker级并发:这是由Bull的concurrency设置控制的。它定义了单个进程中,可以同时运行多少个作业。
@Processor('video-encoding', { concurrency: 2, // 这个视频编码处理器,同时只处理2个作业 }) export class VideoProcessor { @Process('encode') async handleEncode(job: Job) { // CPU密集型的视频编码任务 } } @Processor('email') export class EmailProcessor { // 不指定concurrency,默认是1 @Process('send') async handleSend(job: Job) { // I/O密集型的邮件发送任务 } }

踩坑记录:我曾将一个CPU密集型任务的concurrency设置为10,导致服务器负载飙升,所有任务都因超时失败。而I/O密集型任务(如网络请求)可以设置较高的并发(如10-20),因为它们大部分时间在等待,不会阻塞事件循环。黄金法则:CPU密集型任务设置低并发(接近CPU核心数),I/O密集型任务可设置高并发。

3.2.3 使用“暂停/恢复”进行手动流量控制在某些场景下,比如下游依赖服务维护,你需要临时停止处理某个队列的任务。Bull提供了pause()resume()方法。

// 在某个管理服务中 constructor(@InjectQueue('email') private emailQueue: Queue) {} async pauseEmailQueueForMaintenance(duration: number) { await this.emailQueue.pause(true); // true表示等待当前活跃任务完成 this.logger.log(`Email queue paused. Maintenance window: ${duration}ms`); // 设置一个定时器,自动恢复 setTimeout(() => { this.emailQueue.resume(); }, duration); }

3.3 作业选项深度解析:让任务行为更可控

defaultJobOptions和每个queue.add()时传入的选项,是精细控制每个任务行为的开关。

3.3.1 重试与退避策略这是处理瞬时故障(网络抖动、第三方服务短暂不可用)的核心机制。

const jobOptions = { attempts: 5, // 最多尝试5次(包括第一次执行) backoff: { type: 'exponential', // 指数退避。还有'fixed'(固定间隔)和'linear'(线性增长) delay: 3000, // 首次重试延迟3秒 }, removeOnComplete: true, // 成功完成后删除作业(节省Redis空间) removeOnFail: 50, // 保留最近50个失败作业 }; await this.queue.add('process-data', data, jobOptions);
  • 指数退避计算:第一次重试在3秒后,第二次在3 * 2 = 6秒后,第三次在3 * 2^2 = 12秒后,以此类推。这能有效避免在服务恢复的瞬间被重试请求再次打垮。
  • removeOnFail权衡:设为false会保留所有失败记录,可能导致Redis内存增长。建议根据业务重要性设置一个合理的数量,并配套一个清理脚本。

3.3.2 超时、延迟与优先级

await this.queue.add( 'send-reminder', data, { delay: 24 * 60 * 60 * 1000, // 延迟24小时发送 timeout: 30000, // 作业执行超过30秒视为失败 priority: 1, // 优先级,数字越大优先级越高(与直觉相反,需注意) lifo: false, // 默认false是FIFO(先进先出),true则后进先出 } );

注意事项priority只在作业进入队列时排序一次。如果一个高优先级作业在延迟中,新来的低优先级作业可能会被先处理。对于需要严格按时间顺序执行的任务,不要依赖优先级,而应使用delay或外部调度器。

4. 错误处理、监控与可观测性构建

4.1 坚如磐石的错误处理策略

Bull作业的失败分为几种情况:主动抛出错误、Promise被拒绝、超时、进程崩溃。我们的目标是:可预见的错误自动重试,不可预见的错误快速失败并告警。

4.1.1 在Processor内部进行结构化错误处理

@Process('sync-user-data') async handleSyncUserData(job: Job<UserSyncData>): Promise<void> { try { // 1. 数据验证 if (!isValid(job.data)) { // 数据错误,重试无意义,直接失败 throw new Error(`Invalid job data for job ${job.id}`); } // 2. 业务逻辑,区分可重试错误和不可重试错误 const result = await someExternalApiCall(job.data); if (result.status === 'rate_limited') { // 触发限流,这是一个可重试的瞬时错误 throw new RateLimitError('API rate limit exceeded'); } if (result.status === 'invalid_auth') { // 认证失败,重试没用,需要人工干预 throw new FatalError('Authentication failed, check credentials'); } // 3. 处理成功 await this.db.save(result); job.log(`Successfully synced user ${job.data.userId}`); } catch (error) { this.logger.error(`Job ${job.id} failed`, { error: error.message, stack: error.stack, data: job.data, attemptsMade: job.attemptsMade, }); // 根据错误类型决定是否要重试 if (error instanceof RateLimitError) { // 让Bull的重试机制处理 throw error; } else if (error instanceof FatalError) { // 致命错误,立即失败,不移入重试队列 await job.moveToFailed(error, true); // true表示不重试 } else { // 其他未知错误,也抛出,由Bull根据配置重试 throw error; } } }

通过自定义错误类型(RateLimitError,FatalError),你可以更精细地控制作业的生命周期。

4.1.2 全局失败监听与告警集成除了在Processor内部处理,还需要一个全局的兜底监听。

@Injectable() export class QueueMonitorService implements OnModuleInit { constructor(@InjectQueue('*') private queues: Queue[]) {} // 注入所有队列 onModuleInit() { for (const queue of this.queues) { queue.on('failed', async (job, err) => { // 当作业达到最大重试次数后最终失败,会到达这里 await this.alertService.sendCriticalAlert({ title: `Job Failed Permanently: ${queue.name}/${job.name}`, details: { jobId: job.id, data: job.data, error: err.message, stack: err.stack, attemptsMade: job.attemptsMade, }, }); // 可选:将关键任务的最终失败记录到数据库,以便后续手动重试 if (queue.name === 'payment-callback') { await this.failedJobRepo.save({ queue: queue.name, jobId: job.id, data: job.data, error: err.message }); } }); queue.on('stalled', (jobId) => { // 作业被标记为“停滞”(进程可能意外退出),需要监控 this.logger.warn(`Job ${jobId} has stalled`); }); } } }

4.2 全面的监控与度量

没有度量,就无法优化,也无法快速发现问题。

4.2.1 使用Bull提供的MetricsBull队列本身提供了丰富的状态信息,可以通过queue.getJobCounts()等方法获取。

@Injectable() export class QueueMetricsService { constructor(@InjectQueue('email') private emailQueue: Queue) {} async getQueueHealth() { const counts = await this.emailQueue.getJobCounts(); const metrics = { waiting: counts.waiting, // 等待中 active: counts.active, // 活跃/执行中 completed: counts.completed, // 已完成 failed: counts.failed, // 已失败 delayed: counts.delayed, // 延迟中 }; // 计算一些衍生指标 const totalProcessed = metrics.completed + metrics.failed; const failureRate = totalProcessed > 0 ? (metrics.failed / totalProcessed) * 100 : 0; return { ...metrics, failureRate: `${failureRate.toFixed(2)}%`, isHealthy: metrics.waiting < 100 && failureRate < 5, // 自定义健康标准 }; } }

可以定期(如每分钟)运行这个检查,并将数据推送到Prometheus、Datadog等监控系统,绘制队列长度、失败率等图表。

4.2.2 自定义性能度量在作业开始和结束时打点,记录耗时。

@Process('generate-report') async handleGenerateReport(job: Job) { const startTime = Date.now(); try { // ... 生成报告的逻辑 const report = await this.reportService.generate(job.data); const duration = Date.now() - startTime; this.metricsService.histogram('job.duration', duration, { queue: 'report', jobName: 'generate' }); this.metricsService.increment('job.success', 1, { queue: 'report' }); return report; } catch (error) { const duration = Date.now() - startTime; this.metricsService.histogram('job.duration', duration, { queue: 'report', jobName: 'generate' }); this.metricsService.increment('job.failure', 1, { queue: 'report', errorType: error.constructor.name }); throw error; } }

4.3 日志与追踪

在微服务架构下,一个用户请求可能触发多个队列作业,串联这些日志需要分布式追踪。

4.3.1 注入追踪上下文在向队列添加作业时,传递追踪ID(如X-Request-Id)。

// 在HTTP请求处理器中 @Post('order') async createOrder(@Body() dto, @Headers('x-request-id') requestId: string) { const order = await this.orderService.create(dto); await this.emailQueue.add('send-receipt', { orderId: order.id, _trace: { // 约定一个字段存放追踪信息 requestId, spanId: generateSpanId(), }, }, { jobId: `email-receipt-${order.id}`, // 设置可读的jobId,便于搜索 }); }

在Processor中,取出这个上下文,并设置到你的日志记录器或追踪客户端中。

@Process('send-receipt') async handleSendReceipt(job: Job) { const { _trace } = job.data; // 使用AsyncLocalStorage或类似技术,在整个异步链路中传递trace this.tracingService.setTrace(_trace); this.logger.log(`Processing receipt for order ${job.data.orderId}`, { jobId: job.id, ..._trace }); // ... 发送邮件逻辑 }

5. 运维、调试与灾难恢复

5.1 管理界面与手动操作

虽然Bull有丰富的API,但在生产环境中,一个可视化的管理界面对于运维和调试至关重要。

5.1.1 集成Arena或Bull-BoardArenaBull-Board是流行的Bull队列可视化工具。在NestJS中集成它们非常简单,通常作为一个独立的HTTP端点。

// arena.config.ts (或 bull-board类似) import Arena from 'bull-arena'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrapArena() { const app = await NestFactory.create(AppModule); const arena = Arena({ Bull: require('bull'), // 或 BullMQ queues: [ { name: 'email', hostId: 'MyNestJSApp', redis: { host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }, }, // ... 其他队列 ], }); app.use('/arena', arena); // 挂载到 /arena 路径 await app.listen(4567); // 在一个非业务端口启动 } bootstrapArena();

通过这个界面,你可以查看所有队列的状态、作业详情、手动重试失败作业、暂停/恢复队列,极大地提升了运维效率。

5.1.2 常见的CLI与管理命令除了UI,也要熟悉Bull的API,以便编写脚本。

// 脚本示例:清理旧的成功作业 async function cleanOldJobs(queueName: string, olderThanDays: number) { const queue = new Queue(queueName, { redis }); const jobs = await queue.getJobs(['completed'], 0, -1); // 获取所有已完成作业 const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000; for (const job of jobs) { if (job.finishedOn < cutoff) { await job.remove(); // 删除作业 console.log(`Removed old job ${job.id}`); } } }

5.2 应对灾难场景

5.2.1 Redis故障转移如果使用Redis哨兵或集群,Bull可以自动处理故障转移。确保你的redis配置中包含了相关设置。

redis: { sentinels: [ { host: 'sentinel1.example.com', port: 26379 }, { host: 'sentinel2.example.com', port: 26379 }, ], name: 'mymaster', // 主节点名称 }

5.2.2 进程崩溃与作业恢复Bull的作业是持久化的。如果Worker进程崩溃,Bull会检测到“停滞”(stalled)的作业,并根据设置(默认30秒后)将其重新放回队列,由其他健康的Worker进程处理。确保你的作业是幂等的,即同一任务被多次执行不会产生副作用,这是应对任何重试机制的基础。

5.2.3 数据迁移与版本兼容性当作业数据结构发生变化时(例如,新增了一个字段),旧版本Worker处理新作业,或新版本Worker处理旧作业都可能出错。

  • 策略一:版本化作业名queue.add('process-v2', data)。新旧Processor可以共存一段时间。
  • 策略二:数据兼容性处理:在Processor开始处,对job.data进行校验和转换。
@Process('process-data') async handleProcess(job: Job<any>) { // 使用any或联合类型 const data = this.dataMigrationService.normalize(job.data); // 使用标准化后的data进行业务逻辑 }

5.3 测试策略

队列系统的测试需要特殊考虑。

5.3.1 单元测试(Processor逻辑)使用内存版的Redis(如ioredis-mock)或在测试中模拟Queue实例。

describe('EmailProcessor', () => { let processor: EmailProcessor; let mockJob: Partial<Job>; beforeEach(() => { processor = new EmailProcessor(mockEmailService); mockJob = { id: 'test-1', data: { userId: 1, email: 'test@test.com' }, log: jest.fn(), progress: jest.fn(), }; }); it('should send email successfully', async () => { await processor.handleWelcomeEmail(mockJob as Job); expect(mockEmailService.send).toHaveBeenCalledWith('test@test.com', expect.anything()); }); });

5.3.2 集成测试(队列流程)在测试环境中启动一个真实的Redis实例(可以使用Docker Testcontainers),测试完整的“投递-处理”流程。

it('should process a job from queue', async () => { const testQueue = new Queue('test-integration', { redis: testRedisClient }); const testProcessor = new TestProcessor(); // 模拟NestJS的装饰器行为(简化) testQueue.process('test', (job) => testProcessor.handle(job)); // 添加作业 await testQueue.add('test', { foo: 'bar' }); // 等待一段时间让作业被处理 await new Promise(resolve => setTimeout(resolve, 1000)); const jobCounts = await testQueue.getJobCounts(); expect(jobCounts.completed).toBe(1); });

成为一名真正的“队列低语者”,远不止是调用几个API。它要求你深入理解异步任务的生命周期、资源管理、错误传播和分布式系统的复杂性。在NestJS的优雅抽象之上,结合Bull提供的强大原语,通过精细的配置、严谨的错误处理、全面的监控和健全的运维实践,你才能将异步任务队列从潜在的“性能瓶颈”和“故障源头”,转变为支撑应用弹性和可扩展性的坚实骨架。记住,驯服“狂牛”的关键在于预见、度量与控制,而这正是专业开发与业余尝试的分水岭。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/27 18:59:18

Nodejs后端服务如何集成Taotoken提供稳定的AI功能支持

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 Node.js 后端服务如何集成 Taotoken 提供稳定的 AI 功能支持 对于使用 Node.js 构建后端服务的开发者而言&#xff0c;将大模型能力…

作者头像 李华
网站建设 2026/5/27 18:57:34

京东购物评价自动化解决方案:告别手动评价烦恼

京东购物评价自动化解决方案&#xff1a;告别手动评价烦恼 【免费下载链接】jd_AutoComment 自动评价,仅供交流学习之用 项目地址: https://gitcode.com/gh_mirrors/jd/jd_AutoComment 还在为购物后堆积如山的评价任务而头疼吗&#xff1f;每次大促过后&#xff0c;面对…

作者头像 李华
网站建设 2026/5/27 18:56:12

Python 3.10.0 环境搭建实战:从零配置到首个程序运行

1. Python 3.10.0 环境搭建全流程指南 刚接触Python的小伙伴们&#xff0c;是不是对如何安装配置一头雾水&#xff1f;别担心&#xff0c;今天我就带大家手把手完成Python 3.10.0的环境搭建。这个版本在错误提示、类型系统等方面都有显著改进&#xff0c;特别适合新手入门。我会…

作者头像 李华
网站建设 2026/5/27 18:55:20

Maven命令

将jar包部署到私服&#xff1a;mvn deploy –Dmaven.test.skiptrueidea maven 仓库 jar 包下载不来下解决方案&#xff1a;mvn -U idea:ideamaven查询版本mvn dependency:tree | grep spring

作者头像 李华