
异步任务提交 Redis 状态轮询模式实战指南一、概述当一个业务操作耗时较长如拆单发货、大文件处理、复杂计算如果同步执行会导致接口超时或用户长时间等待。解决方案是接口只负责提交任务立即返回一个任务ID实际处理交给 MQ 消费者异步执行处理结果写入 Redis前端通过任务ID轮询查询结果。本文以一个订单拆分处理场景为例系统介绍这种模式的完整实现。二、核心设计2.1 两个接口的职责接口职责执行方式返回提交接口参数校验 → 初始化状态 → 发MQ同步毫秒级taskId状态查询接口根据 taskId 读取 Redis同步毫秒级处理状态结果2.2 状态流转提交接口写入 Redis: status1处理中 ↓ MQ 消费者处理成功: status2成功 result ↓ 或 MQ 消费者处理失败: status0失败 errorMsg errorCode2.3 数据流向提交接口: 前端 → Controller → 写Redis(status1) → 发MQ → 返回taskId异步处理: MQ Consumer → 业务处理 → 更新Redis(status2或0)状态查询: 前端 → Controller → 读Redis → 返回结果三、Redis 数据结构设计DatapublicclassTaskStatusDtoimplementsSerializable{/** 状态1-处理中 2-成功 0-失败 */privateIntegerstatus;/** 错误码失败时返回用于前端特殊处理 */privateIntegererrorCode;/** 错误信息失败时返回 */privateStringerrorMsg;/** 处理结果成功时返回 */privateTaskResultDtoresult;}Redis Key 设计task:{memberId}_{orderCode}_{uuid} TTL: 1天包含 memberId 和 orderCode 便于排查问题UUID 保证唯一性。注博客https://blog.csdn.net/badao_liumang_qizhi四、完整示例4.1 提交接口RestControllerRequestMapping(/api/page/order)publicclassOrderSplitController{ResourceprivateStringRedisTemplatestringRedisTemplate;ResourceprivateOrderSplitMqSenderorderSplitMqSender;privatestaticfinalStringREDIS_KEY_FORMATorder:split:%s;/** * 提交拆单请求. */PostMapping(/submit-order-split)publicRestControllerResultStringsubmitOrderSplit(RequestBodyOrderSplitParamDtoparamDto){// 1. 参数校验if(CheckEmptyUtil.isEmpty(paramDto.getOrderCode())){thrownewBusinessException(订单编号不能为空);}// 2. 生成任务IDRedis KeyIntegermemberIdUserContext.getMemberId();StringuuidmemberId_paramDto.getOrderCode()_UUID.randomUUID().toString().replaceAll(-,);StringtaskIdString.format(REDIS_KEY_FORMAT,uuid);// 3. 初始化Redis状态处理中TaskStatusDtostatusDtonewTaskStatusDto();statusDto.setStatus(1);// 处理中stringRedisTemplate.opsForValue().set(taskId,JSON.toJSONString(statusDto),1,TimeUnit.DAYS);// 4. 补充参数paramDto.setTaskId(taskId);paramDto.setOperatorId(UserContext.getUserId());paramDto.setMemberId(memberId);// 5. 发送MQorderSplitMqSender.send(paramDto);// 6. 返回taskIdreturnRestControllerResult.success(taskId);}/** * 查询拆单状态. */GetMapping(/order-split-status)publicRestControllerResultTaskStatusDtogetOrderSplitStatus(RequestParam(taskId)StringtaskId){if(CheckEmptyUtil.isEmpty(taskId)){thrownewBusinessException(taskId不能为空);}StringjsonstringRedisTemplate.opsForValue().get(taskId);TaskStatusDtostatusDto;if(CheckEmptyUtil.isEmpty(json)){// Redis中无数据可能已过期statusDtonewTaskStatusDto();statusDto.setStatus(0);statusDto.setErrorCode(-1);statusDto.setErrorMsg(任务不存在或已过期);}else{statusDtoJSON.parseObject(json,TaskStatusDto.class);}returnRestControllerResult.success(statusDto);}}4.2 MQ 生产者Slf4jComponentpublicclassOrderSplitMqSender{ResourceprivateRabbitTemplaterabbitTemplate;privatestaticfinalStringEXCHANGEorder.split.exchange;privatestaticfinalStringROUTING_KEYorder.split.routing;publicvoidsend(OrderSplitParamDtoparamDto){StringmessageJSON.toJSONString(paramDto);rabbitTemplate.convertAndSend(EXCHANGE,ROUTING_KEY,message);log.info(拆单任务MQ发送成功, orderCode{}, taskId{},paramDto.getOrderCode(),paramDto.getTaskId());}}4.3 MQ 消费者Slf4jComponentpublicclassOrderSplitMqConsumer{ResourceprivateOrderSplitServiceorderSplitService;ResourceprivateStringRedisTemplatestringRedisTemplate;RabbitListener(queues${mq.order.split.queue})publicvoidconsume(Stringmessage){OrderSplitParamDtoparamDtoJSON.parseObject(message,OrderSplitParamDto.class);StringtaskIdparamDto.getTaskId();log.info(拆单任务消费开始, orderCode{},paramDto.getOrderCode());try{// 执行拆单业务逻辑TaskResultDtoresultorderSplitService.doSplit(paramDto);// 处理成功更新RedisTaskStatusDtostatusDtonewTaskStatusDto();statusDto.setStatus(2);statusDto.setResult(result);stringRedisTemplate.opsForValue().set(taskId,JSON.toJSONString(statusDto));log.info(拆单任务处理成功, orderCode{},paramDto.getOrderCode());}catch(Exceptione){log.warn(拆单任务处理失败, orderCode{},paramDto.getOrderCode(),e);// 处理失败更新Redis区分错误码TaskStatusDtostatusDtonewTaskStatusDto();statusDto.setStatus(0);if(einstanceofBusinessException){statusDto.setErrorCode(((BusinessException)e).getErrorCode());statusDto.setErrorMsg(e.getMessage());}else{statusDto.setErrorCode(-1);statusDto.setErrorMsg(处理异常请稍后重试);}stringRedisTemplate.opsForValue().set(taskId,JSON.toJSONString(statusDto));}}}4.4 业务处理Service逐笔catch模式当任务内部包含多笔处理且希望单笔失败不影响其他笔时Slf4jServicepublicclassOrderSplitServiceImplimplementsOrderSplitService{OverrideTransactional(rollbackForException.class)publicTaskResultDtodoSplit(OrderSplitParamDtoparamDto){ListSplitItemDtoitemsparamDto.getItems();intsuccessNum0;interrorNum0;interrorCode0;ListFailItemDtofailListnewArrayList();for(SplitItemDtoitem:items){try{// 单笔处理逻辑含各种校验processSingleItem(paramDto,item);successNum;}catch(Exceptione){log.warn(单笔拆单失败, itemCode{},item.getItemCode(),e);errorNum;// 记录失败信息FailItemDtofailDtonewFailItemDto();failDto.setItemCode(item.getItemCode());failDto.setErrorMsg(e.getMessage());failList.add(failDto);// 捕获自定义错误码if(einstanceofBusinessException){errorCode((BusinessException)e).getErrorCode();}}}// 组装结果TaskResultDtoresultnewTaskResultDto();result.setTotalNum(items.size());result.setSuccessNum(successNum);result.setErrorNum(errorNum);result.setFailList(failList);// 如果有自定义错误码设置到result中if(errorCode!0){result.setErrorCode(errorCode);}returnresult;}}五、错误码传递机制5.1 问题异步场景下业务异常在 MQ 消费者中抛出前端无法通过 HTTP 响应码直接感知。需要一种机制将错误码传递到前端。5.2 两种传递路径路径A外层异常整个任务失败异常抛出 → Consumer catch → 写入 TaskStatusDto.errorCode → Redis → 前端轮询获取路径B内层异常单笔失败任务整体成功异常抛出 → 内层 catch → 写入 TaskResultDto.errorCode → Redis → 前端从 result.errorCode 获取5.3 前端判断逻辑constpollResultasync(taskId){constresawaitapi.getStatus(taskId);if(res.status1){// 处理中继续轮询return;}if(res.status0){// 整体失败if(res.errorCode10001){showBillUnpaidDialog();// 跳转结算中心}else{showError(res.errorMsg);}return;}if(res.status2){// 整体成功但可能部分失败if(res.result.errorCode10001){showBillUnpaidDialog();// 部分失败因为账单未支付}showResult(res.result);}};六、关键设计点6.1 Redis TTL 设置// 提交时设置 TTL 1天stringRedisTemplate.opsForValue().set(taskId,json,1,TimeUnit.DAYS);// 消费者更新时不重设 TTL保持原有过期时间stringRedisTemplate.opsForValue().set(taskId,json);建议提交时设 1 天 TTL消费者更新时不改 TTL超过 1 天未处理完的任务自动失效6.2 分布式锁防重复处理try(DistributedLocklocklockProvider.getLock(lockKey,TimeUnit.MINUTES,20)){if(lock.tryLock(TimeUnit.MINUTES,10)){// 执行业务}}防止同一任务被多个消费者实例重复处理。6.3 事务与 Redis 写入的关系Transactional(rollbackForException.class)publicTaskResultDtodoSplit(OrderSplitParamDtoparamDto){// 业务处理...}注意如果方法标注了Transactional在方法内部写入 Redis 后如果事务回滚Redis 中的数据不会回滚。建议成功时在事务提交后通过回调写入 Redis失败时在 catch 中写入 Redis此时事务已回滚6.4 前端轮询策略letretryCount0;constmaxRetry60;// 最多轮询60次2分钟constinterval2000;// 2秒一次consttimersetInterval(async(){retryCount;constresawaitapi.getStatus(taskId);if(res.status!1||retryCountmaxRetry){clearInterval(timer);if(retryCountmaxRetryres.status1){showError(处理超时请稍后在历史记录中查看);}else{handleResult(res);}}},interval);七、异常处理分层层级异常类型处理方式对前端的影响提交接口参数校验失败直接抛异常接口返回错误接口报错不进入轮询MQ消费者外层获取锁失败、未知异常写入 status0 errorMsg轮询拿到失败结果MQ消费者内层单笔业务异常记录到 failList errorCode轮询拿到部分成功结果八、与纯同步接口的对比维度同步接口异步提交轮询接口响应时间与处理时间成正比固定毫秒级超时风险高无错误码传递HTTP 响应中直接返回写入 Redis轮询获取用户体验页面卡住等待显示进度/加载状态实现复杂度低中适用场景处理时间 3秒处理时间 3秒九、最佳实践清单提交接口只做参数校验和发MQ不执行业务逻辑Redis Key 包含业务标识memberId、orderCode便于排查设置合理的 Redis TTL避免数据堆积分布式锁防重复消费内层 catch 记录 errorCode不仅记录 errorMsg前端设置轮询上限避免无限轮询Redis 数据不存在时返回明确状态任务不存在或已过期区分整体失败和部分失败给前端不同的处理依据事务提交后再写 Redis保证数据一致性日志记录完整链路提交时记录 taskId消费时记录开始/成功/失败