[{"data":1,"prerenderedAt":371},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Ffitst-experience-with-bullmq":3,"surround:\u002Fposts\u002F2025\u002Ffitst-experience-with-bullmq":359},{"id":4,"title":5,"body":6,"canonical":338,"categories":339,"date":341,"description":342,"draft":343,"extension":344,"image":338,"meta":345,"navigation":343,"path":346,"permalink":338,"readingTime":347,"recommend":338,"references":338,"seo":352,"sitemap":353,"stem":354,"tags":355,"type":338,"updated":341,"__hash__":358},"content\u002Fposts\u002F2025\u002Ffitst-experience-with-bullmq.md","BullMQ 初体验",{"type":7,"value":8,"toc":317},"minimark",[9,22,30,33,38,41,82,86,100,103,106,113,117,133,136,139,143,153,156,162,165,171,174,180,187,193,196,199,203,209,212,218,221,224,232,235,241,247,262,268,271,275,282,285,288,296,305,312],[10,11,12,13,17,18,21],"p",{},"在之前使用 nestjs 做一个项目时用到了队列的功能。官方推荐的包是 ",[14,15,16],"code",{"code":16},"@nestjs\u002Fbullmq","。用下来还是比较方便的，我们今天一起来看看它底层的 ",[14,19,20],{"code":20},"BullMQ"," 这个包的一些内容。",[23,24],"link-card",{"className":25,"icon":28,"link":29,"title":20},[26,27],"gradient-card","active","https:\u002F\u002Fbullmq.io\u002Fimages\u002Fbullmq-logo.png","https:\u002F\u002Fbullmq.io\u002F",[10,31,32],{},"在使用之前，我们要明确一些事情！",[34,35,37],"h2",{"id":36},"_1-为什么要有消息队列","1. 为什么要有消息队列？",[10,39,40],{},"这里照搬 BullMQ 官网的说法：",[42,43,44,52,58,64,70,76],"ol",{},[45,46,47,51],"li",{},[48,49,50],"strong",{},"解耦组件","\n消息队列是解耦应用程序组件的绝佳方式。它们也是通过将负载分配到多个工作进程来扩展应用程序的绝佳方式。",[45,53,54,57],{},[48,55,56],{},"提高可靠性","\n通过向与其他服务或组件通信的作业添加重试和延迟来提高可靠性。",[45,59,60,63],{},[48,61,62],{},"将任务拆分为更小的部分","\n将一个大的任务分解成更小的、独立的任务，可以更容易地扩展你的应用程序，并且在其中一个任务失败时也能提高应用程序的可靠性。",[45,65,66,69],{},[48,67,68],{},"卸载长时间运行的任务","\n在后台运行长时间运行的任务，让用户在任务于工作进程池中运行时继续使用您的应用程序。",[45,71,72,75],{},[48,73,74],{},"遵守现有的 API","\n通过在应用程序调用外部 API 之间添加队列并启用速率限制，来限制应用程序调用外部 API 的速度。",[45,77,78,81],{},[48,79,80],{},"安排作业","\n作业可以延迟执行，以便在特定时间点运行，或者您可以按给定的时间间隔运行作业。",[34,83,85],{"id":84},"_2-在什么时候使用","2. 在什么时候使用",[42,87,88,91,94,97],{},[45,89,90],{},"如果你有一些特别耗时的任务，而且用户不需要立即知道处理结果（文件处理——音视频压缩处理等）",[45,92,93],{},"用户注册后需要发送短信、邮件等",[45,95,96],{},"缓存突发流量，避免系统过载",[45,98,99],{},"日志收集等操作",[10,101,102],{},"接下来我们看一下 BullMQ 的简单使用。",[34,104,105],{"id":105},"关联包安装",[42,107,108],{},[45,109,110],{},[48,111,112],{},"bullmq",[114,115],"copy",{"code":116},"pnpm install bullmq",[42,118,120],{"start":119},2,[45,121,122,125,126],{},[48,123,124],{},"redis"," 这里选择 ",[127,128,132],"a",{"href":129,"rel":130},"https:\u002F\u002Fgithub.com\u002Fredis\u002Fioredis",[131],"nofollow","ioredis",[114,134],{"code":135},"pnpm install ioredis",[34,137,138],{"id":138},"基础语法",[140,141,142],"h3",{"id":142},"添加任务",[144,145,151],"pre",{"className":146,"code":148,"language":149,"meta":150},[147],"language-js","import { Queue } from 'bullmq';\n\nconst myQueue = new Queue('foo');\n\nasync function addJobs() {\n    await myQueue.add('myJobName', { foo: 'bar' });\n    await myQueue.add('myJobName', { qux: 'baz' }, { delay: 5000 }); \u002F\u002F 延迟作业，该任务将至少等待 5 秒钟才会进行处理\n}\n\nawait addJobs();\n","js","",[14,152,148],{"__ignoreMap":150},[140,154,155],{"id":155},"消费",[144,157,160],{"className":158,"code":159,"language":149,"meta":150},[147],"import { Worker } from 'bullmq';\nimport IORedis from 'ioredis';\n\nconst connection = new IORedis({ maxRetriesPerRequest: null });\n\nconst worker = new Worker(\n    'foo',\n    async (job) => {\n    \u002F\u002F 先打印  { foo: 'bar'}\n    \u002F\u002F 然后打印 { qux: 'baz' }\n        console.log(job.data);\n    },\n    { connection },\n);\n",[14,161,159],{"__ignoreMap":150},[140,163,164],{"id":164},"监听任务状态",[144,166,169],{"className":167,"code":168,"language":149,"meta":150},[147],"\u002F\u002F 成功\nworker.on('completed', (job) => {\n    console.log(`${job.id} has completed!`);\n});\n\n\u002F\u002F 失败\nworker.on('failed', (job, err) => {\n    console.log(`${job.id} has failed with ${err.message}`);\n});\n",[14,170,168],{"__ignoreMap":150},[140,172,173],{"id":173},"自动删除已完成的任务",[144,175,178],{"className":176,"code":177,"language":149,"meta":150},[147],"await myQueue.add(\n    'test',\n    { foo: 'bar' },\n    { removeOnComplete: true, removeOnFail: true }, \u002F\u002F 成功和失败的任务都删除\n);\n",[14,179,177],{"__ignoreMap":150},[10,181,182,183,186],{},"添加任务是异步的，我们可以使用 ",[14,184,185],{"code":185},"addBulk"," 进行批量添加",[144,188,191],{"className":189,"code":190,"language":149,"meta":150},[147],"import { Queue } from 'bullmq';\n\nconst queue = new Queue('paint');\n\nconst name = 'jobName';\nconst jobs = await queue.addBulk([\n    { name, data: { paint: 'car' } },\n    { name, data: { paint: 'house' } },\n    { name, data: { paint: 'boat' } },\n]);\n",[14,192,190],{"__ignoreMap":150},[140,194,195],{"id":195},"速率限制",[10,197,198],{},"我们可以配置在一段时间内处理多少个作业",[200,201,202],"h4",{"id":202},"全局",[144,204,207],{"className":205,"code":206,"language":149,"meta":150},[147],"import { Queue } from 'bullmq';\n\n\u002F\u002F 1 秒处理一个\nawait queue.setGlobalRateLimit(1, 1000);\n",[14,208,206],{"__ignoreMap":150},[200,210,211],{"id":211},"单个任务",[144,213,216],{"className":214,"code":215,"language":149,"meta":150},[147],"import { QueueScheduler, Worker } from 'bullmq';\n\nconst worker = new Worker('painter',\n    async job => paintCar(job),\n    {\n        limiter: {\n            max: 10,\n            duration: 1000,\n        },\n    });\n",[14,217,215],{"__ignoreMap":150},[140,219,220],{"id":220},"在失败时重试",[10,222,223],{},"什么情况下 BullMQ 认为是失败了",[42,225,226,229],{},[45,227,228],{},"Worker 中抛出异常",[45,230,231],{},"Worker 陷入停滞",[10,233,234],{},"如何配置失败时重试？",[236,237,238],"blockquote",{},[10,239,240],{},"固定重试3次，每次延迟3秒",[144,242,245],{"className":243,"code":244,"language":149,"meta":150},[147],"import { Queue } from 'bullmq';\n\nconst myQueue = new Queue('foo');\n\nawait queue.add(\n    'test-retry',\n    { foo: 'bar' },\n    {\n        attempts: 3,\n        backoff: {\n            type: 'fixed',\n            delay: 1000,\n        },\n    },\n);\n",[14,246,244],{"__ignoreMap":150},[10,248,249,250,253,254,257,258,261],{},"可以配置 ",[14,251,252],{"code":252},"jitter"," 来实现随机抖动。延迟时间会在 ",[14,255,256],{"code":256},"1000ms"," ~ ",[14,259,260],{"code":260},"1000 * 0.5 ms"," 之间随机",[144,263,266],{"className":264,"code":265,"language":149,"meta":150},[147],"import { Queue } from 'bullmq';\n\nconst myQueue = new Queue('foo');\n\nawait queue.add(\n    'test-retry',\n    { foo: 'bar' },\n    {\n        attempts: 8,\n        backoff: {\n            type: 'fixed',\n            delay: 1000,\n            jitter: 0.5,\n        },\n    },\n);\n",[14,267,265],{"__ignoreMap":150},[34,269,270],{"id":270},"扩展",[140,272,274],{"id":273},"使用-bull-board-监控和管理","使用 bull-board 监控和管理",[10,276,277],{},[127,278,281],{"href":279,"rel":280},"https:\u002F\u002Fgithub.com\u002Ffelixmosh\u002Fbull-board",[131],"bull-board",[10,283,284],{},"它可以结合很多 node 框架使用。比如有常见的 express, fastify, koa, nestjs, hono, h3, elysia 等",[10,286,287],{},"我这几天看 hono 有点多，这里使用 hono 扩展。",[144,289,294],{"className":290,"code":292,"language":293},[291],"language-text","pnpm install @bull-board\u002Fhono\n","text",[14,295,292],{"__ignoreMap":150},[144,297,303],{"className":298,"code":300,"filename":301,"language":302,"meta":150},[299],"language-ts","import { createBullBoard } from '@bull-board\u002Fapi'\nimport { BullMQAdapter } from '@bull-board\u002Fapi\u002FbullMQAdapter'\nimport { HonoAdapter } from '@bull-board\u002Fhono'\nimport { serveStatic } from '@hono\u002Fnode-server\u002Fserve-static'\n\nimport { emailQueue } from '@\u002Fqueues\u002Femail.queue'\n\nconst serverAdapter = new HonoAdapter(serveStatic)\n\nserverAdapter.setBasePath('\u002Fadmin\u002Fqueues')\n\ncreateBullBoard({ queues: [new BullMQAdapter(emailQueue)], serverAdapter })\n\nexport const queueUI = serverAdapter.registerPlugin()\n","routes\u002Fadmin\u002Fqueue.ts","ts",[14,304,300],{"__ignoreMap":150},[144,306,310],{"className":307,"code":308,"filename":309,"language":302,"meta":150},[299],"import { createApp } from '@\u002Flib\u002Fcreate-app'\nimport { queueUI } from '@\u002Froutes\u002Fadmin\u002Fqueue'\n\nconst app = createApp()\n\napp.route('\u002Fadmin\u002Fqueues', queueUI)\n\nexport { app }\n","app.ts",[14,311,308],{"__ignoreMap":150},[313,314],"pic",{"caption":315,"src":316},"bull-board 首页","https:\u002F\u002Fbitmc.uno\u002Fpicgo\u002Fbull-board.png",{"title":150,"searchDepth":318,"depth":318,"links":319},4,[320,321,322,323,335],{"id":36,"depth":119,"text":37},{"id":84,"depth":119,"text":85},{"id":105,"depth":119,"text":105},{"id":138,"depth":119,"text":138,"children":324},[325,327,328,329,330,334],{"id":142,"depth":326,"text":142},3,{"id":155,"depth":326,"text":155},{"id":164,"depth":326,"text":164},{"id":173,"depth":326,"text":173},{"id":195,"depth":326,"text":195,"children":331},[332,333],{"id":202,"depth":318,"text":202},{"id":211,"depth":318,"text":211},{"id":220,"depth":326,"text":220},{"id":270,"depth":119,"text":270,"children":336},[337],{"id":273,"depth":326,"text":274},null,[340],"代码","2025-12-29 10:36:19","BullMQ 是一个快速且强大的 Redis™ 后台作业处理库。",true,"md",{},"\u002Fposts\u002F2025\u002Ffitst-experience-with-bullmq",{"text":348,"minutes":349,"time":350,"words":351},"6 min read",5.275,316500,1055,{"title":5,"description":342},{"loc":346},"posts\u002F2025\u002Ffitst-experience-with-bullmq",[112,356,357,124],"queue","nodejs","QBid1oej91tLO5sDTV7-85yvv99LH-1XVYIUB2A4q38",[360,366],{"title":361,"path":362,"stem":363,"date":364,"type":365,"children":-1},"廿七碎笔","\u002Fposts\u002F2025\u002F27-year-old","posts\u002F2025\u002F27-year-old","2025-12-25 10:24:40","story",{"title":367,"path":368,"stem":369,"date":370,"type":338,"children":-1},"Nestjs + Drizzle 项目中一些常用的自定义校验","\u002Fposts\u002F2025\u002Fsnippets-nestjs-drizzle-custom-validation","posts\u002F2025\u002Fsnippets-nestjs-drizzle-custom-validation","2025-12-30 17:14:59",1789642588023]