基于SpringBoot的在线废品回收系统设计与实现(2025毕设论文):高并发下单与调度效率优化实战
1. 废品回收业务里的“慢”到底卡在哪
做毕设时,我最初把系统当成普通电商:用户下单 → 后台派单 → 回收员上门。结果一压测,问题全暴露:
- 高并发抢单:同一秒 200 人下单,订单号重复、库存超卖,数据库唯一索引疯狂抛
DuplicateKeyException。 - 调度链路长:下单后要“选回收员 → 算距离 → 推消息 → 等确认”,同步走完平均 1.2 s,接口 RT 99 线直接飙红。
- 热数据反复查:回收员实时坐标每 5 s 上报一次,前端每 3 s 轮询“谁离我最近”,Redis QPS 被冲到 4 w+,CPU 利用率 90 %。
一句话:没有并发控制、没有异步化、没有空间索引,系统“假死”。
2. 技术选型:别让中间件成为第二瓶颈
2.1 消息队列:RabbitMQ vs Kafka
- Kafka 吞吐高,但最小延迟 5 ms 以上,且分区数固定后无法弹性收缩;回收场景峰值只在早晚两个“垃圾时段”,其余时间流量低,分区资源浪费。
- RabbitMQ 支持单队列 3 w TPS,延迟可压到 1 ms 内,队列级 TTL+死信队列天然适合“订单 30 min 未接单自动取消”业务。
结论:选 RabbitMQ,用延迟队列做超时回滚,比定时任务扫表轻量得多。
2.2 分布式锁:Redis vs ZooKeeper
- ZK 写模型重,锁竞争场景下 QPS 2 k 左右就顶不住;且 ZK 客户端胖,毕业部署在 1C2G 学生机,内存吃紧。
- Redis + Redisson 把锁竞争转 Lua 脚本,单节点 5 w QPS 无压力,支持“看门狗”自动续期,防止业务 GC 停顿导致锁过期。
结论:选 Redis,Redisson的RLock即可。
3. 核心模块落地细节
3.1 幂等下单:Redisson 分布式锁 + 令牌桶防重
思路:
“用户 ID + 地址 MD5” 作为锁 key,有效期 5 s;同一用户 5 s 内只能创建一单,解决前端重复提交 + 脚本刷单。
关键代码(Controller 层):
@RestController @RequestMapping("/order") public class OrderController { @Resource private RedissonClient redisson; @Resource private OrderService orderService; @PostMapping public ApiResp<Long> create(@Valid @RequestBody CreateOrderCmd cmd, @LoginUser Long userId){ String lockKey = "order:uid:" + userId + ":addr:" + Md5Util.md5(cmd.getAddress()); RLock lock = redisson.getLock(lockKey); boolean locked = lock.tryLock(0, 5, TimeUnit.SECONDS); if(!locked) throw new BizException("操作太频繁,请5秒后再试"); try{ // 再查一次兜底,防止锁竞争边界 Long orderId = orderService.createOrder(cmd, userId); return ApiResp.success(orderId); }finally { if(lock.isHeldByCurrentThread()) lock.unlock(); } } }注意:
- 锁等待时间给 0,意味着抢不到立即拒绝,用户体验比“转圈 5 s”更好。
- Service 层再做一次
exists查询,防止锁竞争瞬间的极小缝隙。
3.2 就近派单:GeoHash + 环形队列
思路:
- 回收员端每 5 s 上报坐标 → GeoHash 长度为 6(约 1.2 km 网格)→ 写 Redis
GEOADD。 - 下单后系统根据用户地址计算同网格 & 八邻域网格内回收员 → 按距离排序 → 取前 5 个 → 推异步消息到个人队列。
代码片段(Service):
@Service public class DispatchService { @Resource private StringRedisTemplate redis; @Resource private RabbitTemplate rabbit; private static final int GEO_HASH_PREC = 6; private static final double RADIUS_KM = 3.0; public void dispatch(Long orderId, BigDecimal lat, BigDecimal lng){ // 1. 计算中心+邻居共9个格子 GeoHash center = GeoHash.withCharacterPrecision(lat.doubleValue(), lng.doubleValue(), GEO_HASH_PREC); Set<String> neighbors = center.getAdjacent().stream() .map(GeoHash::toBase32) .collect(Collectors.toSet()); neighbors.add(center.toBase32()); // 2. 批量取回收员坐标 List<Point> candidates = new ArrayList<>(); for(String geo : neighbors){ Set<String> members = redis.opsForSet().members("geo:collector:" + geo); if(members==null) continue; members.forEach(m -> { List<String> ll = redis.opsForGeo().position("geo:collector", m); if(ll.size()==2){ double dist = DistanceUtils.getDistance(lat, lng, Double.parseDouble(ll.get(1)), Double.parseDouble(ll.get(0))); if(dist <= RADIUS_KM * 1000) candidates.add(new Point(m, dist)); } }); } // 3. 按距离升序取5人 candidates.sort(Comparator.comparingDouble(p -> p.dist)); List<String> top5 = candidates.stream().limit(5).map(p -> p.member).collect(Collectors.toList()); // 4. 发消息 top5.forEach(c -> rabbit.convertAndSend("dispatch.collector." + c, orderId, m -> {m.getMessageProperties().setExpiration("300000"); return m;})); } @Data @AllArgsConstructor private static class Point{ String member; double dist;} }亮点:
- 用 GeoHash 前缀把二维搜索降成一维
Set过滤,O(N)变O(1)。 - 消息 TTL 5 min,超时未接单自动流入死信队列,触发“取消订单 & 退款”逻辑,无需定时任务。
3.3 异步消息驱动:订单状态机解耦
状态流转:CREATED → DISPATCHED → RECEIVED → FINISHED
所有状态变更走 MQ,Consumer 幂等通过“订单状态版本号”字段保证:
update t_order set status=#{target}, version=version+1 where id=#{id} and version=#{oldVersion}返回影响行数 0 即重复消费,直接 ack。
4. 压测结果:TPS 翻 3 倍,RT 降 60 %
环境:
Mac M1 笔记本 Docker 限 4C8G,MySQL 8.0 + Redis 6.2 + RabbitMQ 3.11,JMeter 200 线程循环 5 min。
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 平均下单 RT | 1100 ms | 180 ms |
| TPS | 186 | 620 |
| 错误率(重复单) | 4.3 % | 0 % |
| CPU(MySQL) | 92 % | 38 % |
结论:
分布式锁解决并发写冲突,Redis GEO 把距离计算从应用层搬到内存,MQ 异步化削峰,数据库压力骤降。
5. 安全与稳定性:学生项目也不能裸奔
- 接口限流:
网关层用 Bucket4j + Redis 令牌桶,单 IP 10 r/s,超出返回 429,防止脚本刷单。 - XSS 过滤:
统一 JSON 解析器加JacksonXssCleanDeserializer,对地址、备注字段白名单标签外全部转义。 - 消息丢失补偿:
RabbitMQ 开启 publisher confirm + 持久化队列;本地建t_mq_log表,confirm 成功才更新状态,定时扫描未确认记录重发。 - 缓存穿透:
回收员 ID 不存在时布隆过滤器先挡,再缓存短时间的空对象({"":-1}),过期 30 s,防止海量非法 ID 打群架。
6. 生产环境避坑清单
- Redisson 看门狗续期 30 s,GC 超 30 s 会锁漂移;建议
-XX:MaxGCPauseMillis=100,或关闭看门狗改固定过期。 - GeoHash 边界问题:相邻格子可能在江对岸,务必加“直线距离二次过滤”,避免给回收员派 3 km 直线、8 km 车程的单子。
- 消息 TTL 与队列 TTL 区别:前者单条消息、后者整个队列;若误配队列 TTL,会导致未接单订单整批取消。
- 分库分表后,订单表按 user_id 分片,但派单查询需按地理范围走,用 ES + Geo 索引或把“市+区”作为分片键冗余冗余字段,避免跨分片扫描。
- 学生机内存小,RabbitMQ 流控阈值调低(
vm_memory_high_watermark.relative = 0.4),否则大消息积压直接 OOM。
7. 留给读者的思考题
当前架构在单城市跑得很欢,如果业务扩张到“多城市分片部署”,你会怎么玩?
- 地理分片:按城市拆独立 RabbitMQ vhost,还是共用一套集群、用
city_code做 routing? - 数据同步:用户跨城下单,订单表是否需要全局二级索引?Redis GEO 是否做跨城缓存复制?
- 调度策略:热门城市回收员密度高,冷门城市稀疏,如何动态调整“就近”阈值,保证两边效率均衡?
把这套模板改造成真正的全国服务,挑战才刚刚开始。祝你毕设答辩顺利,也欢迎把新的踩坑故事分享回来。