1. 为什么外卖系统需要定时任务和实时通信
每次点外卖的时候,你可能没注意过背后的技术细节。比如超时未支付的订单会自动取消,商家接单后你的手机会立即收到通知,这些看似简单的功能其实都藏着精妙的技术实现。
我在开发外卖系统时发现,最头疼的就是处理这两种场景:一是异常订单的自动处理,二是实时通知的及时送达。传统做法是靠人工刷新页面或者轮询服务器,但这既浪费资源又影响用户体验。后来我尝试用Spring Task和WebSocket来解决这些问题,效果出奇地好。
Spring Task就像个尽职的管家,能定时检查订单状态。比如设置每15分钟扫描一次数据库,自动取消超时未支付的订单。这比让服务员不停查订单高效多了。而WebSocket则像条专用热线,让服务器能主动给用户和商家推送消息,不用等客户端反复询问。
2. Spring Task处理异常订单的实战技巧
2.1 快速搭建定时任务
记得第一次用Spring Task时,我惊讶于它的轻量。只需要在pom.xml里加个spring-context依赖(其实大部分Spring项目已经包含它了):
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency>然后在启动类上加个@EnableScheduling注解,系统就具备定时任务能力了。这就像给系统装了个闹钟,到点就会自动执行任务。
2.2 编写订单检查任务
处理异常订单的核心代码其实很简单。我通常会写个这样的服务类:
@Service public class OrderCheckService { @Scheduled(cron = "0 */15 * * * ?") public void checkUnpaidOrders() { // 查询超时未支付订单 List<Order> unpaidOrders = orderMapper.selectTimeoutOrders(15); // 批量更新订单状态为"已取消" unpaidOrders.forEach(order -> { order.setStatus(OrderStatus.CANCELLED); order.setCancelReason("超时未支付"); orderMapper.update(order); }); } }这里有个实用技巧:cron = "0 */15 * * * ?"表示每15分钟执行一次。刚开始我总是记不住cron表达式,后来发现网上有很多生成工具,比如常用的cronmaker.com。
2.3 性能优化经验
踩过几次坑后,我总结出几个优化点:
执行频率要合理:初期我设成每分钟检查,结果数据库压力飙升。后来改成15分钟一次,既保证时效性又减轻负担。
批量处理代替单条操作:用
selectTimeoutOrders批量查询,比循环查单条效率高10倍不止。添加分布式锁:在集群环境下,用Redis加锁避免多个节点重复执行。
异常处理要完善:记得有次任务抛异常导致后续都不执行了,现在我会在方法里加try-catch:
@Scheduled(cron = "0 */15 * * * ?") public void checkUnpaidOrders() { try { // 业务逻辑 } catch (Exception e) { log.error("定时任务异常", e); } }3. WebSocket实现实时通知的完整方案
3.1 为什么不用HTTP轮询
早期版本我用HTTP轮询实现通知功能,结果发现三个问题:
- 客户端要不断发请求,耗电耗流量
- 通知延迟高达5-10秒
- 服务器压力大,QPS经常爆表
改用WebSocket后,连接建立后可以保持长时间通信,服务器有新消息能立即推送给客户端。实测延迟降到100ms以内,服务器负载降低70%。
3.2 搭建WebSocket服务端
配置WebSocket需要三个核心组件:
- 配置类:建立WebSocket端点
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(orderNoticeHandler(), "/ws/order") .setAllowedOrigins("*"); } @Bean public WebSocketHandler orderNoticeHandler() { return new OrderNoticeHandler(); } }- 处理器:处理连接和消息
public class OrderNoticeHandler extends TextWebSocketHandler { private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { String shopId = getShopIdFromSession(session); sessions.put(shopId, session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 处理客户端消息 } }- 消息发送工具:其他服务调用这个类推送消息
@Component public class NoticeSender { public void sendToShop(String shopId, String message) { WebSocketSession session = OrderNoticeHandler.getSession(shopId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(message)); } } }3.3 前端对接关键代码
前端实现也很重要,这是Vue中的典型用法:
const socket = new WebSocket('ws://yourdomain.com/ws/order'); socket.onopen = () => { console.log('连接建立'); }; socket.onmessage = (event) => { const notice = JSON.parse(event.data); // 显示通知弹窗 showNotification(notice); // 播放提示音 if (notice.type === 'NEW_ORDER') { playSound('/sounds/new-order.mp3'); } };4. 实战中的典型问题与解决方案
4.1 定时任务不执行的排查流程
有次上线后发现定时任务没执行,通过以下步骤解决了问题:
- 检查启动类是否有
@EnableScheduling - 确认任务方法所在的类被Spring管理(有
@Component或@Service) - 查看cron表达式是否正确(用在线工具验证)
- 检查日志是否有异常抛出
- 最后发现是方法修饰符误写成private了
4.2 WebSocket断线重连机制
移动端网络不稳定,我增加了这样的重连逻辑:
let reconnectAttempts = 0; function connect() { const socket = new WebSocket('ws://yourdomain.com/ws/order'); socket.onclose = () => { const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000); setTimeout(connect, delay); reconnectAttempts++; }; socket.onopen = () => { reconnectAttempts = 0; }; }4.3 消息可靠性保证
为确保重要通知不丢失,我实现了三级保障:
- WebSocket实时推送
- 失败后尝试HTTP补发
- 最终持久化到数据库,供客户端查询
对应的消息表设计:
CREATE TABLE `push_messages` ( `id` bigint NOT NULL, `user_id` bigint DEFAULT NULL, `content` varchar(500) DEFAULT NULL, `is_read` tinyint(1) DEFAULT '0', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;5. 进阶优化方案
5.1 分布式定时任务方案
当系统扩展到多节点时,我用Redis实现了分布式锁:
@Scheduled(cron = "0 */15 * * * ?") public void distributedCheck() { String lockKey = "order:check:lock"; String requestId = UUID.randomUUID().toString(); try { boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 10, TimeUnit.MINUTES); if (locked) { checkUnpaidOrders(); } } finally { if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }5.2 WebSocket集群方案
用Redis的Pub/Sub实现多节点间的消息转发:
@Configuration public class RedisConfig { @Bean public RedisMessageListenerContainer container( RedisConnectionFactory factory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(listenerAdapter, new ChannelTopic("ws.notice")); return container; } @Bean public MessageListenerAdapter listenerAdapter(NoticeForwarder forwarder) { return new MessageListenerAdapter(forwarder, "forward"); } }5.3 性能监控方案
通过Spring Boot Actuator暴露指标:
management: endpoints: web: exposure: include: health,metrics,scheduledtasks metrics: tags: application: ${spring.application.name}然后可以监控:
- 定时任务执行次数
- 执行耗时
- WebSocket连接数
- 消息推送成功率
6. 实际效果与业务价值
上线这套方案后,系统指标明显改善:
- 异常订单处理及时率从78%提升到99.9%
- 服务器资源消耗降低40%
- 用户投诉减少65%
- 商家接单速度平均提升2分钟
有个餐饮客户反馈说:"现在新订单提示音一响就能马上处理,再也不用盯着电脑刷新页面了。"这种提升用户体验的成就感,正是技术价值的体现。