一、核心目标
- 解决大单据量提交(网关超时>10秒)问题,将请求响应时间控制在500ms内
- 基于现有 Redis/MySQL 实现,无额外中间件成本
- 保证数据可靠性:单据不丢失、不重复处理、状态一致
- 支持高并发:适配日均1000-10万条单据提交,峰值50+条/秒
二、整体架构
复制代码
前端 → API网关 → 单据提交服务(Spring Boot)→ MySQL(存单据)→ Redis(消息队列+状态缓存)→ 单据处理服务(多实例)→ 业务执行(库存更新/流水生成)
↓ ↑
单据查询服务 ← Redis(状态缓存)← MySQL(更新状态)← 处理结果回调
三、核心模块设计
(一)单据提交服务(生产端)
职责
接收前端请求 → 基础校验 → 生成单据ID → 落地MySQL → 发送Redis队列 → 快速响应
关键逻辑
- 单据ID生成:
业务前缀(IN/OUT)+ 时间戳(毫秒)+ 4位随机数,确保全局唯一
- 幂等性保障:
- 前端:提交按钮防抖(3秒内不可重复点击)
- 后端:MySQL 建唯一索引
uk_bill_no_submitter(业务单号+提交人),防止重复提交
- 数据落地顺序:先存MySQL(状态
PENDING),再发Redis队列,确保数据不丢失
- 核心代码片段:
java
复制代码
// 1. 校验+生成ID
validateBill(dto); // 幂等+格式校验
String billId = "IN_" + System.currentTimeMillis() + RandomUtils.nextInt(1000, 9999);
// 2. 落地MySQL
billMapper.insert(Bill.builder().id(billId).status("PENDING").data(JSON.toJSONString(dto)).build());
// 3. 发Redis队列(List结构,LPUSH入队)
redisTemplate.opsForList().leftPush("warehouse:bill:queue", billId);
// 4. 缓存状态(Redis有效期24小时)
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, "PENDING", 24, TimeUnit.HOURS);
// 5. 快速响应
return Result.success("单据提交成功", billId);
(二)Redis 队列核心设计
数据结构选型(4个核心Key)
| Redis类型 |
Key名称 |
用途 |
操作方式 |
| List |
warehouse:bill:queue |
主队列(待处理单据ID) |
LPUSH入队、BRPOP阻塞出队 |
| Sorted Set |
warehouse:bill:retry |
重试队列(处理失败单据) |
ZADD(score=下次重试时间戳)、ZRANGEByScore查询 |
| List |
warehouse:bill:dead |
死信队列(重试3次失败) |
LPUSH入队、人工导出处理 |
| String |
warehouse:bill:lock:{billId} |
分布式锁(防重复处理) |
SETNX(过期30秒)、删除释放锁 |
Redis 配置要求(保证可靠性)
- 开启 AOF 持久化(
appendonly yes),同步策略 appendfsync everysec
- 开启 RDB 快照(
save 60 1000),双重保障数据不丢失
- 内存策略
maxmemory-policy allkeys-lru,避免OOM
(三)单据处理服务(消费端)
职责
- 阻塞读取Redis队列 → 分布式锁防重复 → 业务处理 → 状态更新 → 失败重试/死信
核心流程(多实例部署)
- 主队列消费(单线程阻塞,避免空轮询):
java
复制代码
@PostConstruct
public void startConsume() {
new Thread(() -> {
while (true) {
try {
// 1. BRPOP阻塞读取(无消息时阻塞,0=永久阻塞)
String billId = redisTemplate.opsForList().rightPop("warehouse:bill:queue", 0, TimeUnit.SECONDS);
if (StringUtils.isEmpty(billId)) continue;
// 2. 分布式锁:防止重复处理(30秒过期,覆盖最大处理耗时)
String lockKey = "warehouse:bill:lock:" + billId;
String lockValue = UUID.randomUUID().toString();
Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (!lockSuccess) {
// 锁失败:放回队列,重新排队
redisTemplate.opsForList().leftPush("warehouse:bill:queue", billId);
continue;
}
try {
// 3. 业务处理(核心逻辑)
processBill(billId);
} catch (Exception e) {
// 4. 失败:入重试队列
handleRetry(billId, e);
} finally {
// 5. 释放锁(避免死锁)
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
} catch (Exception e) {
log.error("消费异常", e);
Thread.sleep(1000); // 异常后休眠,避免循环报错
}
}
}).start();
}
- 业务处理核心步骤:
java
复制代码
private void processBill(String billId) {
// 1. 查询单据(仅处理PENDING状态)
Bill bill = billMapper.selectById(billId);
if (bill == null || !"PENDING".equals(bill.getStatus())) return;
// 2. 更新状态为"处理中"
bill.setStatus("PROCESSING");
billMapper.updateById(bill);
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, "PROCESSING");
// 3. 核心业务(入库/出库:库存更新、流水生成)
BillDTO dto = JSON.parseObject(bill.getData(), BillDTO.class);
inventoryService.updateStock(dto); // 库存操作(加事务)
generateStockFlow(billId, dto); // 生成流水
// 4. 处理成功:更新状态为SUCCESS
bill.setStatus("SUCCESS");
bill.setFinishTime(new Date());
billMapper.updateById(bill);
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, "SUCCESS");
}
- 重试/死信处理:
java
复制代码
private void handleRetry(String billId, Exception e) {
Bill bill = billMapper.selectById(billId);
int retryCount = bill.getRetryCount() == null ? 0 : bill.getRetryCount() + 1;
if (retryCount <= 3) {
// 重试:10秒后重新入队(Sorted Set按时间排序)
long nextRetryTime = System.currentTimeMillis() + 10 * 1000;
redisTemplate.opsForZSet().add("warehouse:bill:retry", billId, nextRetryTime);
// 更新重试次数和失败原因
bill.setRetryCount(retryCount);
bill.setFailReason(e.getMessage().substring(0, 200));
billMapper.updateById(bill);
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, "RETRY_" + retryCount);
} else {
// 死信:入死信队列,状态设为FAIL
redisTemplate.opsForList().leftPush("warehouse:bill:dead", billId);
bill.setStatus("FAIL");
bill.setFailReason("重试3次失败:" + e.getMessage().substring(0, 200));
billMapper.updateById(bill);
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, "FAIL");
}
}
- 重试队列消费(单独线程):
java
复制代码
@PostConstruct
public void startRetryConsume() {
new Thread(() -> {
while (true) {
try {
// 查询当前时间前的重试消息
long now = System.currentTimeMillis();
Set<String> billIds = redisTemplate.opsForZSet().rangeByScore("warehouse:bill:retry", 0, now);
if (CollectionUtils.isEmpty(billIds)) {
Thread.sleep(2000);
continue;
}
// 放回主队列重新消费
for (String billId : billIds) {
redisTemplate.opsForZSet().remove("warehouse:bill:retry", billId);
redisTemplate.opsForList().leftPush("warehouse:bill:queue", billId);
}
} catch (Exception e) {
log.error("重试队列消费异常", e);
Thread.sleep(1000);
}
}
}).start();
}
(四)单据查询服务
职责
核心逻辑(缓存优先)
java
复制代码
@GetMapping("/{billId}/status")
public Result getStatus(@PathVariable String billId) {
// 1. 优先查Redis缓存
String status = redisTemplate.opsForValue().get("warehouse:bill:status:" + billId);
if (StringUtils.isNotEmpty(status)) {
Bill bill = billMapper.selectById(billId);
return Result.success(status, bill.getFailReason());
}
// 2. 缓存未命中查MySQL
Bill bill = billMapper.selectById(billId);
if (bill == null) return Result.fail("单据不存在");
// 3. 缓存结果
redisTemplate.opsForValue().set("warehouse:bill:status:" + billId, bill.getStatus(), 24, TimeUnit.HOURS);
return Result.success(bill.getStatus(), bill.getFailReason());
}
四、关键技术保障
1. 数据可靠性(不丢失)
- 单据先存MySQL再入队,Redis崩溃可从MySQL恢复
- Redis开启AOF+RDB双持久化,队列数据不丢失
- 死信队列兜底,重试3次失败仍可人工处理
2. 避免重复处理
- Redis
rightPop 原子性,多实例不会重复获取同一单据
- 分布式锁(SETNX)+ 单据状态校验(仅处理PENDING),双重防重复
- MySQL乐观锁(可选),更新状态时校验版本号
3. 高并发支持
- Redis List 单实例TPS万级,支撑高并发提交
- 处理服务多实例部署,水平扩展消费能力
- 批量处理(重试队列批量放回主队列),提升效率
五、运维监控要点
1. 核心监控指标
| 指标 |
监控对象 |
告警阈值 |
| 主队列长度 |
LLEN warehouse:bill:queue |
>500条 |
| 死信队列长度 |
LLEN warehouse:bill:dead |
>10条 |
| 处理失败率 |
失败单据数/总单据数 |
>5% |
| Redis 内存使用率 |
INFO memory → used_memory_ratio |
>80% |
| 单据处理时长 |
处理完成时间-提交时间 |
>5分钟 |
2. 日常运维操作
- 定期清理Redis过期缓存(状态缓存24小时自动过期)
- 每日检查死信队列,处理失败单据(导出后修复重新入队)
- 监控Redis持久化日志,确保AOF/RDB正常生成
- 处理服务多实例部署(建议2-3个),避免单点故障
六、常见问题与解决方案
| 问题场景 |
原因分析 |
解决方案 |
| 单据提交后状态一直PENDING |
1. Redis队列未消费;2. 处理服务宕机 |
1. 检查消费线程是否运行;2. 重启处理服务;3. 手动触发队列消费 |
| 重复处理单据 |
分布式锁失效/状态校验遗漏 |
1. 检查锁过期时间(≥业务最大耗时);2. 强化状态校验(仅处理PENDING) |
| Redis队列堆积 |
消费能力不足 |
1. 增加处理服务实例;2. 优化业务处理逻辑(如批量处理) |
| 单据丢失 |
先入队后存MySQL,Redis崩溃 |
调整顺序:先存MySQL再入队;定时任务扫描MySQL未入队单据重新入队 |
七、方案优势总结
- 零额外成本:复用现有Redis/MySQL,无需引入MQ
- 高可靠:双持久化+死信队列+状态校验,无消息丢失/重复处理
- 高性能:Redis原子操作+多实例消费,支撑高并发
- 易落地:开发成本低,核心逻辑清晰,运维简单