后台异步任务最容易骗过人的指标是队列长度。pending=0 只能说明这一刻没有积压,不能说明失败任务被处理了,也不能说明重复消费、重试间隔、禁用 Topic 和误投递都没问题。更稳的做法是把 Worker 数、最大重试次数、重试间隔按 Topic 管起来,再把统计、测试投递和配置修改放进有权限控制的后台入口。XYGo Admin 的实体关系要说清:它是 GoFrame + Vue3 的后台项目,队列配置要和 RBAC 权限、菜单接口一起落库,而不是单独做一个消息队列产品。master 分支最近这组队列相关提交,可以当成一个小样本看:消息结构保留 Topic/Retry/MaxRetry,配置逻辑负责加载 Topic 参数,迁移脚本把配置表和队列管理权限一起落库。它适合中小后台任务,不是 Kafka 或 RabbitMQ 的替代品。
先别急着盯 pending
很多后台系统加异步任务时,第一版通常长这样:业务代码里 Push() 一条消息,消费者起一个 goroutine,页面上最多放一个 pending 数。这个版本能跑,但它回答不了几个上线前必须回答的问题:
- 失败后要不要重试?重试几次?
- 每个 Topic 的并发 Worker 是一样的吗?
- Topic 临时禁用后,测试投递接口还能不能继续塞消息?
- pending 掉到 0,是成功消费了,还是失败后被丢掉了?
- Redis 挂了,磁盘队列、报错路径和恢复流程是否一致?
如果这些问题都靠代码里的常量和口头约定处理,队列页面再漂亮也只是一个计数器。后台异步任务真正难的不是投递,而是失败语义和运维边界。
一个轻量队列至少要保留的字段
从源码证据看,server/internal/library/queue/queue.go 里的 Message 没有只存 body,而是把运行语义放进消息结构:
go
type Message struct {
Topic string `json:"topic"`
Body string `json:"body"`
Retry int `json:"retry"`
MaxRetry int `json:"maxRetry"`
CreatedAt int64 `json:"createdAt"`
}
这几个字段比 pending 数更重要。Topic 决定谁来消费;Retry 和 MaxRetry 决定失败是否还有下一次机会;CreatedAt 至少能给排查延迟和积压留下时间线。源码里还把驱动抽成 Driver,保留 Push、Pop、Len 三个接口,并支持 Redis List 和 Disk 两种驱动。这里没有把轻量队列包装成大而全的平台,反而比较克制:它只保证中小后台任务的投递、消费、长度读取和延迟投递扩展点。
只看 Len() 会漏掉失败和重试语义。更合理的后台统计应该把 pending、deadSize、consumeRate 和最近错误一起看。pending 降了但 deadSize 增了,用户看到的仍然是"任务没完成"。
max_retry 不应该散落在消费者里
第二项证据是 server/internal/logic/queue/config.go。这里的 Bootstrap 先同步注册 Topic,再加载配置,然后把配置交给队列库,最后启动消费者。Reload 也不是只改数据库,而是重新加载配置并重启或刷新消费者。
这说明一个取舍:最大重试次数、重试间隔、Worker 数这类参数,不适合每个消费者自己写死。它们更像 Topic 的运行配置。
go
func Bootstrap(ctx context.Context) error {
if err := SyncRegisteredTopics(ctx); err != nil {
return err
}
configs, err := LoadTopicConfigs(ctx)
if err != nil {
return err
}
queueLib.ApplyTopicConfigs(configs)
queueLib.StartConsumers(ctx)
return nil
}
我更愿意把这些参数放进配置表,而不是藏在消费者代码里。原因很简单:登录日志、通知推送、操作日志的失败成本不一样。通知可以晚一点,操作日志不能随便丢,演示任务又不能影响生产链路。所有 Topic 共用一个重试常量,后面一定会变成"这个任务特殊处理一下"。特殊处理多了,队列就变成了隐形状态机。
配置表要和权限一起验
第三项证据是 server/cmd_tools/migrate/1.4.8_queue_config.mysql.sql。迁移脚本里建了 xy_sys_queue_config,字段包括 workers、max_retry、retry_delay_sec、status,还写入了队列管理菜单的查看和编辑配置权限。
sql
CREATE TABLE IF NOT EXISTS `xy_sys_queue_config` (
`topic` varchar(64) NOT NULL DEFAULT '',
`workers` int(11) NOT NULL DEFAULT 1,
`max_retry` int(11) NOT NULL DEFAULT 3,
`retry_delay_sec` int(11) NOT NULL DEFAULT 0,
`status` tinyint(4) NOT NULL DEFAULT 1,
UNIQUE KEY `uk_topic` (`topic`)
);
INSERT INTO `xy_admin_menu` (...) VALUES
(812, 250, 3, '查看', ... '["GET /admin/queue/stats","GET /admin/queue/topics"]', ...),
(813, 250, 3, '编辑配置', ... '["POST /admin/queue/configSave"]', ...);
这点很容易被忽略。队列配置页面不是普通查询页,改 workers 可能带来并发问题,改 status 可能让任务停掉,测试投递接口也可能误打到生产 Topic。把配置做成页面之后,权限反而比命令行时代更重要。
我的检查顺序通常是这样:
| 检查点 | 只看 pending 会漏掉什么 | 更可靠的验收口径 |
|---|---|---|
| 失败重试 | 失败任务被丢弃 | Retry、MaxRetry、错误日志一起看 |
| Worker 数 | 并发导致重复处理 | Topic 级配置和幂等键一起看 |
| 禁用 Topic | 页面停了但接口还能投递 | status、测试投递、消费端都要拦 |
| 权限 | 任何后台用户都能改配置 | 查看和编辑配置权限分开 |
| 迁移脚本 | MySQL/PG 字段漂移 | 配置表、菜单权限、接口权限一起回归 |
什么时候该换专业 MQ
轻量队列有边界。登录日志、操作日志、站内通知、演示任务、后台导出通知,这些任务通常可以接受短暂延迟,也容易做幂等。用 Redis List 或磁盘兜底就够了,尤其是在单体后台或小规模部署里。
下面这些场景就别硬撑了:支付状态、库存扣减、跨服务事务、严格顺序消费、大吞吐多消费者组、需要消息回溯和复杂死信治理的链路。这里要么上 Kafka、RabbitMQ、Pulsar 这类成熟 MQ,要么至少把幂等、补偿、审计和告警单独设计出来。轻量队列最多解决后台工程里的"能看、能改、能重试",不能替业务兜底。
我会怎么验收一条后台队列
上线前,我会按 Topic 写一张很小的验收表,不追求花哨:
- 正常投递一条任务,确认消费者记录成功日志。
- 人为让消费者返回可重试错误,确认 Retry 增加且不超过 MaxRetry。
- 把 Topic 状态改成禁用,确认页面投递和消费端都不继续执行。
- 调大 workers,确认处理函数有幂等键,重复消息不会造成二次扣减或重复通知。
- 跑一遍迁移脚本,确认配置表、菜单权限和接口权限都存在。
这里的规范来源只放一个:GitHub 仓库。核验时点是 2026-08-08:GitHub Release API 最新公开 Release 仍是 v1.4.6,但 master 分支已有 v1.4.8 队列相关提交,不能把它写成"正式 Release 已发布"。
队列长度可以当告警入口,但不能当验收结论。真正要问的是:失败去哪了,谁能改配置,重试会不会放大事故,轻量队列到哪一步必须停下来换专业 MQ。把这些问题写进后台,比多做一个 pending 数字有用得多。