「生成二维码数据包」(POST /mes/pro/bag-code/generate)与「生成原始数据」(POST /mes/pro/bag-code/raw-generate)在几十万条数据量下直接超时,前端收不到响应。
经代码定位,超时不是单一原因,而是三处叠加:
| 瓶颈 | 位置 | 说明 |
|---|---|---|
| 逐条向 Redis 要码 | MesProBagCodeServiceImpl.java:143-144、176-177 |
每个袋码生成「追溯码 + 业务码」两次,MesCodeRedisDAO.generate() 每次执行一次 Redis SADD 往返(MesCodeRedisDAO.java:53)。30 万袋 = 60 万次网络往返,仅此一步即分钟级 |
| 单事务巨量插入 | MesProBagCodeServiceImpl.java:154、185 |
insertBatch 默认 1000 条/批,但整批在同一个 @Transactional 内完成,几十万行形成超大事务 |
| 同步长请求 | Controller 直接返回生成结果 | 请求线程从头执行到尾;网关/浏览器超时后连接断开,但服务端仍继续写库。用户重试会再生成一整包重复数据,且无幂等保护 |
此外,selectMaxBagNo() 对 mes_pro_bag_code 做 MAX(bag_no),当前表没有 bag_no 索引,数据量上来后每次生成都要全表扫描。
biz:code:used 集合机制。采用与现有二维码导出任务(MesProQrExportTask)一致的「小批量同步、大批量异步」双路径:
前端提交生成
│
├─ 袋码总数 ≤ sync-threshold(默认 20000)
│ └─ 请求线程内完成:批量取码 → 分段落库 → 返回原有结果(async=false)
│
└─ 袋码总数 > sync-threshold
├─ 落任务记录,注册运行态,投递后台线程池,立即返回 taskId + uuid(async=true)
└─ 前端按 taskId 轮询进度;完成后刷新列表;取消则删除已生成的部分数据
同步与异步共用同一套「分段生成」执行器,避免两份逻辑。
| 状态值 | 名称 | 说明 |
|---|---|---|
| 10 | 排队中 | 已落库,等待线程池接管 |
| 20 | 生成中 | 后台线程执行中 |
| 25 | 取消中 | 已收到取消请求,正在停止并清理部分数据 |
| 30 | 已完成 | 全部袋码(及托盘)已落库 |
| 40 | 已取消 | 用户取消,部分数据已清理 |
| 50 | 已失败 | 异常终止,部分数据已清理 |
终态:30 / 40 / 50。
新增 mes_pro_bag_code_generate_task:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| request_id | VARCHAR(64) | 幂等键,唯一索引;前端每次提交生成一个 UUID |
| biz_type | VARCHAR(16) | PACKAGE(数据包)/ RAW(原始数据) |
| uuid | VARCHAR(32) | 本次生成组 UUID(提交时即生成,失败/取消后用于清理) |
| item_id / item_code / item_name | BIGINT / VARCHAR | 品种快照 |
| unit_type | VARCHAR(32) | 计量单位类型(吨 / 48袋 / 100袋) |
| per_pallet | INT | 每托袋数;吨类型为 0 |
| pallet_count | INT | 托盘数;原始数据为 0 |
| total_count | INT | 袋码总数 |
| finished_count | INT | 已生成袋码数 |
| status | TINYINT | 状态,见 4.1 |
| error_msg | VARCHAR(500) | 失败原因 |
| finish_time | DATETIME | 完成/终止时间 |
| creator / create_time / updater / update_time / deleted | — | 沿用 BaseDO |
索引:
UNIQUE KEY uk_request_id (request_id)KEY idx_status (status)KEY idx_uuid (uuid)迁移脚本:sql/mysql/mes_pro_bag_code_generate_task.sql。
新增 KEY idx_bag_no (bag_no),用于 selectMaxBagNo() 与袋序号排序。
迁移脚本:sql/mysql/mes_pro_bag_code_bag_no_index.sql(先查 information_schema.statistics 再建,兼容重复执行)。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /mes/pro/bag-code/generate |
合并后的数据包生成入口 |
| POST | /mes/pro/bag-code/raw-generate |
原始袋码生成入口 |
请求参数不变。响应扩展:
| 字段 | 类型 | 说明 |
|---|---|---|
| async | Boolean | true=已转后台任务;false=本次同步完成 |
| taskId | Long | 异步任务编号;同步时为 null |
| uuid | String | 生成组 UUID,两种模式都返回 |
| type / perPallet | String / Integer | 数据包模式下返回 |
| totalCount | Integer | 袋码总数 |
| palletCount | Integer | 托盘总数;原始数据为 0 |
| palletIds | List<Long> | 仅同步完成时返回;异步模式为 null,避免上千个 ID 撑大响应 |
后端按 totalCount 自动分流,前端不需要各自计算阈值。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /mes/pro/bag-code/generate-task/get?id= |
查询任务状态与进度 |
响应字段:id、bizType、status、statusName、progress(0~100)、totalCount、finishedCount、palletCount、itemId、itemCode、itemName、unitType、perPallet、uuid、errorMsg、createTime、finishTime。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /mes/pro/bag-code/generate-task/cancel?id= |
请求取消;已终态任务拒绝 |
权限:提交/取消用 mes:pro-bag-code:create,查询用 mes:pro-bag-code:query。
totalCount ≤ maxTaskCount(默认 1,000,000),超出直接报错。requestId 非空时先查任务表;命中则直接返回已有任务,不重复生成。并发重复提交由 uk_request_id 唯一索引兜底,捕获冲突后回查。syncThreshold:走同步执行,返回 async=false。syncThreshold:async=true、taskId、uuid、totalCount、palletCount。maxConcurrentTasks(默认 1)个生成任务。占用失败时返回「已有生成任务进行中」并带上当前任务进度。将「逐条 SADD」改为「批量脚本写入」:
ThreadLocalRandom 精确生成 16 位数字)。SADD 到 biz:code:used,返回本批实际写入成功的码。效果:30 万袋从 60 万次网络往返降到约 120 次(每次 5000 个)。
袋码与托盘码都走该批量方法;
generate()保留给其他少量场景。
allocateBagNoStart(totalCount, selectMaxBagNo()),后续各段用 startNo + offset 推算。取消产生的序号空洞允许存在(序号只要求唯一、不回退),因此不要按段重复分配。palletId 与 tempCode 两个数组供袋码段引用,避免堆积整表 DO。chunkSize(默认 5000)分段落库,每段落库后回写一次 finished_count。REQUIRES_NEW 事务中完成,避免几十万行单事务。分段方法必须由执行器跨 Bean 调用,保证事务代理生效。finished_count 每段(5000 条)更新一次,30 万条约 60 次更新,开销可忽略。uuid 下已生成的袋码(DELETE ... WHERE uuid = ? LIMIT 5000 循环);generate_uuid 记录生成组,按 DELETE ... WHERE generate_uuid = ? AND batch_id IS NULL AND code IS NULL LIMIT 5000 循环。该条件确保只删从未入批次的临时托盘,不会误删用户后续已绑定的托盘;说明:生成一半的数据包不可用,必须清理。这里采用物理删除而非逻辑删除,是因为这些行从未对外展示,逻辑删除只会留下几十万条垃圾并浪费索引空间;用户主动删除已生成数据包仍沿用原有逻辑删除逻辑。
uuid 的部分数据,任务置为「已失败」,error_msg 截断到 500 字符。requestId,同一次提交的重试复用同一个值(如网络重试)。requestId 唯一索引保证同一请求只落一个任务;重复提交直接返回已有任务。requestId 时仍可用,但失去网络级幂等保护;前端必须传。新增 mes.bag-code.generate 前缀配置:
| 配置 | 默认值 | 说明 |
|---|---|---|
| sync-threshold | 20000 | 不超过该数量同步生成 |
| chunk-size | 5000 | 每段袋码/每批取码数量 |
| max-concurrent-tasks | 1 | 同时运行的后台生成任务数 |
| max-task-count | 1000000 | 单次生成袋码上限 |
| max-runtime-minutes | 30 | 单任务最长运行时间 |
| poll-interval-seconds | 2 | 仅作为前端轮询建议值下发(可选) |
本节只描述行为与字段,不提供前端代码。
mes/pro/bagCode)generate-form.vue)requestId(UUID),关闭重开则重新生成;同一次提交的自动重试复用同一个 requestId。requestId。async=false:维持现有成功提示与列表刷新行为。async=true:弹窗不立即关闭,展示进度条,按 poll-interval-seconds(建议 2 秒)轮询任务查询接口,直到状态进入终态。errorMsg,允许用户重新提交(重新生成 requestId)。| 字段 | 展示位置 | 说明 |
|---|---|---|
| progress | 生成弹窗 | 进度条百分比 |
| finishedCount / totalCount | 生成弹窗 | 文案“已生成 x / y 条” |
| statusName | 生成弹窗 | 任务状态标签 |
| palletCount | 完成提示 | 数据包模式提示自动生成的托盘数 |
| errorMsg | 生成弹窗 | 失败原因 |
palletIds 为 null,前端不要再用 palletIds.length 判断托盘数,改用 palletCount。| 环节 | 改造前(30 万袋) | 改造后 |
|---|---|---|
| Redis 取码 | 60 万次 RTT,分钟级 | 约 120 次 RTT,秒级 |
| 数据库写入 | 1 个超大事务 | 60 个 5000 行事务 |
| HTTP 响应 | 等待全部完成,必然超时 | 提交后 1 秒内返回 taskId |
| 用户重试 | 产生重复数据包 | 幂等键去重 |
uuid 无残留袋码与托盘,任务状态为已取消。requestId 重复提交只产生一个任务、一个生成组。| 风险 | 应对 |
|---|---|
Redis biz:code:used 集合持续增长(几十万级/次) |
现网已有该机制,集合只存 16 字符短串,单次几十万条约几十 MB;后续可评估按前缀分片,本次不动 |
| 清理 30 万行部分数据耗时 | 分段 DELETE ... LIMIT 5000,避免长事务与主从延迟尖峰 |
bag_no 为 INT,理论上限约 21 亿 |
单次上限 100 万;按当前量级可支撑多年,后续如需长期使用再迁移 BIGINT |
| 异步任务期间服务重启 | 不续跑,标记失败并清理,提示重新生成 |
| 同步路径仍受网关超时约束 | 阈值内单次最多 2 万条,批量取码后预计 1~3 秒,留足余量 |
| 序号 | 内容 | 状态 |
|---|---|---|
| 1 | 方案文档确认 | 已完成 |
| 2 | 新增任务表、袋码 bag_no 索引、托盘 generate_uuid 迁移脚本 |
已完成 |
| 3 | 新增任务 DO / Mapper / 状态枚举 / 配置项 | 已完成 |
| 4 | MesCodeRedisDAO 增加批量取码方法 |
已完成 |
| 5 | 抽取分段生成执行器(同步/异步共用) | 已完成 |
| 6 | 新增任务 Service(提交、查询、取消、回收)与线程池配置 | 已完成 |
| 7 | 改造两个生成接口的同步/异步分流与响应字段 | 已完成 |
| 8 | 新增任务查询/取消接口与权限 | 已完成 |
| 9 | 启动与定时清理任务 | 已完成 |
| 10 | 前端联调(弹窗进度、轮询、取消) | 已完成 |
| 11 | 编译检查与大批量实测 | 已完成(本地 mom-thzy:30 万条约 76 秒生成完毕,提交响应 0.18 秒;取消清理无残留;幂等与并发闸门均验证通过) |
| 脚本 | 说明 |
|---|---|
sql/mysql/mes_pro_bag_code_generate_task.sql |
任务表 |
sql/mysql/mes_pro_bag_code_bag_no_index.sql |
袋码 bag_no 索引 |
sql/mysql/mes_pro_pallet_generate_uuid.sql |
托盘 generate_uuid 列与索引 |