news 2026/7/4 1:50:21

Spring Task定时任务与WebSocket实时通信实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Task定时任务与WebSocket实时通信实战

1. Spring Task 定时任务实战指南

定时任务是后端开发中常见的需求场景,Spring 提供了简单易用的@Scheduled注解来实现定时任务调度。下面我将结合实际项目经验,详细介绍 Spring Task 的使用方法和注意事项。

1.1 定时任务典型应用场景

在实际项目中,我们经常遇到需要定时执行的任务,以下是一些典型场景:

  1. 金融业务场景

    • 信用卡每月还款提醒(每月固定日期发送短信/邮件)
    • 房贷每月还款提醒(每月指定日期扣款)
    • 理财到期提醒(根据产品期限提前通知)
  2. 电商业务场景

    • 未支付订单超时取消(30分钟未支付自动取消)
    • 发货超时提醒(商家超过48小时未发货提醒)
    • 自动确认收货(发货后15天自动确认)
  3. 社交业务场景

    • 生日/纪念日提醒(提前1天发送祝福)
    • 长期未登录用户召回(30天未登录发送召回邮件)
  4. 系统维护场景

    • 每日凌晨执行数据备份
    • 每周清理临时文件
    • 每月生成统计报表

1.2 Cron 表达式详解

Cron 表达式是定时任务的核心,Spring 使用的是标准的 6 位格式:

秒 分 时 日 月 周

常见误区

  • 新手常犯的错误是使用 Quartz 的 7 位格式(包含年字段)
  • 周和日字段通常只定义一个,另一个设为?表示不指定

正确示例

// 每天凌晨1点执行 @Scheduled(cron = "0 0 1 * * ?") public void dailyTask() { // 业务逻辑 } // 每5分钟执行一次 @Scheduled(cron = "0 */5 * * * ?") public void everyFiveMinutes() { // 业务逻辑 }

1.3 定时任务实战案例

下面是一个处理外卖订单状态的定时任务实现:

@Slf4j @Component public class OrderStatusTask { @Autowired private OrderMapper orderMapper; /** * 每小时处理超时未完成的派送中订单 * 将超过1小时未完成的派送中订单自动标记为已完成 */ @Scheduled(cron = "0 0 * * * ?") public void processDeliveryTimeout() { log.info("开始处理超时派送订单..."); // 查询1小时前处于派送中状态的订单 LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1); List<Order> timeoutOrders = orderMapper.findByStatusAndOrderTimeBefore( OrderStatus.DELIVERY_IN_PROGRESS, oneHourAgo ); if (!CollectionUtils.isEmpty(timeoutOrders)) { timeoutOrders.forEach(order -> { order.setStatus(OrderStatus.COMPLETED); order.setCompleteTime(LocalDateTime.now()); orderMapper.update(order); log.info("订单{}超时自动完成", order.getId()); }); } } }

关键点说明

  1. 使用@Component让 Spring 管理任务类
  2. @Scheduled注解标记定时方法
  3. 通过orderMapper操作数据库
  4. 添加详细的日志记录

1.4 定时任务最佳实践

1. 任务幂等性设计

  • 定时任务可能会重复执行,确保业务逻辑可以安全地多次执行
  • 使用状态机控制流程,避免重复处理

2. 异常处理

  • 捕获并记录异常,避免任务中断
  • 重要任务可添加重试机制
@Scheduled(cron = "0 0 2 * * ?") public void importantTask() { try { // 业务逻辑 } catch (Exception e) { log.error("定时任务执行失败", e); // 发送告警通知 alertService.sendAlert("重要任务执行失败", e.getMessage()); } }

3. 分布式环境考虑

  • 单机部署时没问题,但在集群环境下会重复执行
  • 解决方案:
    • 使用分布式锁(Redis/Zookeeper)
    • 使用专门的调度框架(XXL-JOB/Elastic-Job)

4. 性能优化

  • 大数据量处理时分批执行
  • 耗时任务考虑异步执行
@Async @Scheduled(cron = "0 0 3 * * ?") public void heavyTask() { // 耗时操作 }

2. WebSocket 实时通信实战

WebSocket 是实现浏览器和服务器全双工通信的协议,非常适合需要实时交互的场景。

2.1 WebSocket 与 HTTP 对比

特性HTTPWebSocket
通信模式单向请求-响应全双工双向通信
连接建立每次请求新建连接一次握手持久连接
头部开销每次请求完整头部首次握手后仅2-14字节帧头
服务器推送需要轮询或长轮询模拟原生支持
延迟高(频繁建立连接)低(连接复用)
适用场景RESTful API、静态资源实时聊天、股票行情、在线游戏

2.2 WebSocket 服务端实现

下面是一个完整的 WebSocket 服务实现:

@Slf4j @Component @ServerEndpoint("/ws/{userId}") // 定义端点路径,支持路径参数 public class WebSocketServer { // 保存所有会话的线程安全Map private static final ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>(); /** * 连接建立成功回调 */ @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { sessions.put(userId, session); log.info("用户{}连接建立,当前在线人数:{}", userId, sessions.size()); sendMessage(userId, "连接成功"); } /** * 收到客户端消息回调 */ @OnMessage public void onMessage(String message, @PathParam("userId") String userId) { log.info("收到用户{}的消息:{}", userId, message); // 处理业务逻辑... } /** * 连接关闭回调 */ @OnClose public void onClose(@PathParam("userId") String userId) { sessions.remove(userId); log.info("用户{}断开连接,当前在线人数:{}", userId, sessions.size()); } /** * 发生错误回调 */ @OnError public void onError(@PathParam("userId") String userId, Throwable error) { log.error("用户{}的连接发生错误", userId, error); sessions.remove(userId); } /** * 向指定用户发送消息 */ public static void sendMessage(String userId, String message) { Session session = sessions.get(userId); if (session != null && session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("向用户{}发送消息失败", userId, e); } } } /** * 群发消息 */ public static void broadcast(String message) { sessions.forEach((userId, session) -> { if (session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error("群发消息给用户{}失败", userId, e); } } }); } }

2.3 WebSocket 配置类

需要配置 ServerEndpointExporter 来注册 WebSocket 端点:

@Configuration public class WebSocketConfig { /** * 自动注册使用了@ServerEndpoint注解的类 */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } /** * 解决WebSocket中无法注入Spring Bean的问题 */ @Bean public SpringContextHolder springContextHolder() { return new SpringContextHolder(); } } // 用于获取Spring管理的Bean public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } public static <T> T getBean(Class<T> clazz) { return context.getBean(clazz); } }

2.4 WebSocket 客户端实现

前端 JavaScript 连接示例:

// 建立WebSocket连接 const socket = new WebSocket(`ws://${location.host}/ws/${userId}`); // 连接成功回调 socket.onopen = function() { console.log('WebSocket连接已建立'); }; // 接收消息回调 socket.onmessage = function(event) { const message = JSON.parse(event.data); console.log('收到消息:', message); // 更新UI... }; // 连接关闭回调 socket.onclose = function() { console.log('WebSocket连接已关闭'); // 尝试重连... }; // 发送消息 function sendMessage(content) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'message', content: content })); } }

2.5 WebSocket 实战技巧

1. 心跳机制

  • 防止连接因超时被关闭
  • 定时发送ping/pong帧保持连接
// 服务端心跳处理 @OnMessage public void onPong(PongMessage pong, Session session) { // 更新最后活跃时间 } // 定时发送ping scheduledExecutor.scheduleAtFixedRate(() -> { sessions.forEach((id, session) -> { try { session.getBasicRemote().sendPing(ByteBuffer.wrap("ping".getBytes())); } catch (Exception e) { log.error("发送ping失败", e); } }); }, 0, 30, TimeUnit.SECONDS);

2. 消息重连机制

  • 网络不稳定时自动重连
  • 客户端实现指数退避重试
let reconnectAttempts = 0; const maxReconnectAttempts = 5; const reconnectDelay = 1000; // 初始1秒 function connectWebSocket() { const socket = new WebSocket(endpoint); socket.onclose = function() { if (reconnectAttempts < maxReconnectAttempts) { const delay = reconnectDelay * Math.pow(2, reconnectAttempts); reconnectAttempts++; setTimeout(connectWebSocket, delay); } }; }

3. 消息序列化

  • 使用JSON或Protocol Buffers
  • 定义统一的消息格式
public class WebSocketMessage<T> implements Serializable { private String type; // 消息类型 private T data; // 消息内容 private Long timestamp; // getters/setters... }

3. MyBatis 动态SQL实战

MyBatis 的强大之处在于其灵活的动态SQL功能,下面通过实际案例展示如何使用。

3.1 条件查询实现

<!-- 根据条件统计订单金额 --> <select id="sumByMap" resultType="java.lang.Double"> SELECT SUM(amount) FROM orders <where> <if test="begin != null"> AND order_time &gt; #{begin} </if> <if test="end != null"> AND order_time &lt; #{end} </if> <if test="status != null"> AND status = #{status} </if> </where> </select>

注意事项

  1. XML中的特殊符号需要使用转义:

    • &lt;表示 <
    • &gt;表示 >
    • &amp;表示 &
  2. <where>标签会智能处理前缀AND/OR:

    • 如果所有条件都不满足,WHERE关键字不会出现
    • 自动去除第一个条件的AND/OR

3.2 复杂动态SQL示例

<!-- 批量更新订单状态 --> <update id="batchUpdateStatus"> UPDATE orders <set> <if test="status != null">status = #{status},</if> <if test="updateTime != null">update_time = #{updateTime},</if> </set> WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>

关键点

  1. <set>标签会智能处理后缀逗号
  2. <foreach>用于构建IN语句
  3. 参数通过Map或@Param注解传递

3.3 动态SQL最佳实践

1. 避免过度复杂

  • 当条件超过5个时,考虑拆分为多个查询
  • 或者使用<choose>简化逻辑
<select id="findUsers" resultType="User"> SELECT * FROM users <where> <choose> <when test="role == 'admin'"> AND is_admin = 1 </when> <when test="role == 'vip'"> AND is_vip = 1 </when> <otherwise> AND status = 'active' </otherwise> </choose> </where> </select>

2. 性能优化

  • 大量数据时使用分页查询
  • 频繁查询考虑添加索引
<select id="findByPage" resultType="User"> SELECT * FROM users ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>

3. 结果映射

  • 复杂结果使用<resultMap>
  • 关联查询使用<association><collection>
<resultMap id="orderDetailMap" type="Order"> <id property="id" column="id"/> <result property="amount" column="amount"/> <collection property="items" ofType="OrderItem"> <id property="id" column="item_id"/> <result property="productName" column="product_name"/> </collection> </resultMap>

4. Java 8 时间API最佳实践

Java 8 引入了全新的日期时间API,位于java.time包下,解决了旧API的诸多问题。

4.1 核心类对比

描述不可变性时区支持典型用途
LocalDate只包含日期生日、纪念日
LocalTime只包含时间营业时间、会议时间
LocalDateTime包含日期和时间订单创建时间、日志时间
ZonedDateTime带时区的日期时间跨时区会议、航班时刻
Instant时间戳(Unix时间)UTC日志时间戳、性能统计
Duration时间间隔(秒/纳秒)计算两个时间点的差值
Period日期间隔(年/月/日)计算两个日期的差值

4.2 常见操作示例

1. 日期计算

// 获取当前日期 LocalDate today = LocalDate.now(); // 计算明天 LocalDate tomorrow = today.plusDays(1); // 本月最后一天 LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth()); // 计算两个日期之间的天数 long daysBetween = ChronoUnit.DAYS.between(startDate, endDate);

2. 时间格式化

// 日期 -> 字符串 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = LocalDateTime.now().format(formatter); // 字符串 -> 日期 LocalDateTime parsed = LocalDateTime.parse("2023-01-01 12:00:00", formatter);

3. 时区转换

// 获取上海时区的当前时间 ZonedDateTime shanghaiTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); // 转换为纽约时间 ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(ZoneId.of("America/New_York"));

4.3 常见问题解决方案

问题1:日期范围生成错误

错误代码:

for (int i = 1; i <= end.getDayOfMonth() - begin.getDayOfMonth(); i++) { dateList.add(begin.plusDays(i)); }

问题分析

  • getDayOfMonth()只返回月份中的天数(1-31)
  • 跨月计算会得到错误结果

正确方案

LocalDate current = begin; while (!current.isAfter(end)) { dateList.add(current); current = current.plusDays(1); }

问题2:数据库交互

// 保存到数据库 preparedStatement.setObject(1, LocalDateTime.now()); // 从数据库读取 LocalDateTime createTime = resultSet.getObject("create_time", LocalDateTime.class);

问题3:JSON序列化

Spring Boot默认使用Jackson,需要添加依赖:

<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>

配置格式化:

spring: jackson: serialization: write-dates-as-timestamps: false time-zone: Asia/Shanghai

5. Stream API 深度解析

Java 8 的 Stream API 为集合操作提供了函数式编程能力,大幅简化了数据处理代码。

5.1 Stream 操作分类

操作类型返回值说明示例方法
中间操作Stream惰性求值,可链式调用filter, map, sorted
终端操作非Stream触发计算,产生结果或副作用forEach, collect, reduce
短路操作可能提前结束不需要处理全部元素anyMatch, findFirst

5.2 创建Stream的6种方式

// 1. 从集合创建 List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); // 2. 从数组创建 String[] array = {"a", "b"}; Stream<String> stream2 = Arrays.stream(array); // 3. 使用Stream.of Stream<Integer> stream3 = Stream.of(1, 2, 3); // 4. 生成无限流 Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2); Stream<Double> randomStream = Stream.generate(Math::random); // 5. 空流 Stream<String> emptyStream = Stream.empty(); // 6. 从文件创建 Stream<String> lines = Files.lines(Paths.get("data.txt"));

5.3 常用中间操作

1. 过滤与切片

// 过滤偶数 List<Integer> evenNumbers = numbers.stream() .filter(n -> n % 2 == 0) .collect(Collectors.toList()); // 去重 List<String> distinctWords = words.stream() .distinct() .collect(Collectors.toList()); // 分页查询 List<User> page = users.stream() .skip(10) // 跳过前10条 .limit(5) // 取5条 .collect(Collectors.toList());

2. 映射操作

// 提取用户名 List<String> names = users.stream() .map(User::getName) .collect(Collectors.toList()); // 扁平化处理 List<String> allTags = articles.stream() .flatMap(article -> article.getTags().stream()) .distinct() .collect(Collectors.toList()); // 转换为基本类型流 double total = products.stream() .mapToDouble(Product::getPrice) .sum();

3. 排序

// 自然排序 List<String> sortedNames = names.stream() .sorted() .collect(Collectors.toList()); // 自定义排序 List<User> sortedUsers = users.stream() .sorted(Comparator.comparing(User::getAge) .thenComparing(User::getName)) .collect(Collectors.toList()); // 逆序 List<Integer> reversed = numbers.stream() .sorted(Comparator.reverseOrder()) .collect(Collectors.toList());

5.4 常用终端操作

1. 匹配与查找

// 检查是否有管理员 boolean hasAdmin = users.stream() .anyMatch(user -> user.isAdmin()); // 查找第一个VIP用户 Optional<User> firstVip = users.stream() .filter(User::isVip) .findFirst(); // 检查所有用户都激活 boolean allActive = users.stream() .allMatch(User::isActive);

2. 归约操作

// 求和 int sum = numbers.stream() .reduce(0, Integer::sum); // 连接字符串 String concatenated = strings.stream() .reduce("", String::concat); // 最大值 Optional<Integer> max = numbers.stream() .reduce(Integer::max);

3. 收集器

// 转换为List List<String> nameList = users.stream() .map(User::getName) .collect(Collectors.toList()); // 转换为Set Set<String> uniqueNames = users.stream() .map(User::getName) .collect(Collectors.toSet()); // 转换为Map Map<Long, User> userMap = users.stream() .collect(Collectors.toMap(User::getId, Function.identity())); // 分组 Map<Department, List<User>> byDept = users.stream() .collect(Collectors.groupingBy(User::getDepartment)); // 分区 Map<Boolean, List<User>> partitioned = users.stream() .collect(Collectors.partitioningBy(User::isActive)); // 统计 DoubleSummaryStatistics stats = products.stream() .collect(Collectors.summarizingDouble(Product::getPrice));

5.5 并行流注意事项

List<Integer> numbers = /* 大数据量 */; // 使用并行流 int sum = numbers.parallelStream() .mapToInt(Integer::intValue) .sum();

最佳实践

  1. 数据量足够大(通常>10,000)才使用并行流
  2. 避免共享可变状态
  3. 考虑使用线程安全的收集器
// 线程安全收集 ConcurrentMap<Department, List<User>> concurrentMap = users.parallelStream() .collect(Collectors.groupingByConcurrent(User::getDepartment));

5.6 调试技巧

使用peek观察流处理过程:

List<Integer> result = Stream.of(1, 2, 3, 4, 5) .peek(n -> System.out.println("原始: " + n)) .filter(n -> n > 2) .peek(n -> System.out.println("过滤后: " + n)) .map(n -> n * 2) .peek(n -> System.out.println("映射后: " + n)) .collect(Collectors.toList());

6. 项目实战经验总结

在完成"苍穹外卖"项目的过程中,我积累了一些宝贵的实战经验,分享给大家:

6.1 定时任务优化经验

  1. 任务执行时间选择

    • 避开业务高峰期(如外卖午高峰11:00-13:00)
    • 数据统计类任务放在凌晨执行
    • 短周期任务错开时间点(不要所有任务都在整点执行)
  2. 长任务处理

    • 添加@Async注解异步执行
    • 记录任务开始和结束时间
    • 大数据量分批处理
@Async @Scheduled(cron = "0 0 3 * * ?") public void processLargeData() { long start = System.currentTimeMillis(); log.info("大数据处理任务开始"); // 分批处理 int batchSize = 1000; int total = dataMapper.count(); for (int i = 0; i < total; i += batchSize) { List<Data> batch = dataMapper.findBatch(i, batchSize); processBatch(batch); } log.info("任务完成,耗时{}ms", System.currentTimeMillis() - start); }

6.2 WebSocket 性能优化

  1. 连接管理

    • 限制单个IP的最大连接数
    • 实现心跳检测断开无效连接
  2. 消息压缩

    • 大消息使用gzip压缩
    • 二进制协议比JSON更高效
  3. 集群支持

    • 使用Redis Pub/Sub实现跨节点消息广播
    • 会话信息存储到Redis共享
// Redis消息监听器 @Component public class RedisMessageListener { @Autowired private WebSocketServer webSocketServer; @RedisListener(channel = "websocket:notice") public void onMessage(String message) { webSocketServer.broadcast(message); } }

6.3 MyBatis 优化建议

  1. 批量操作
    • 使用<foreach>实现批量插入
    • 开启rewriteBatchedStatements提升性能
<insert id="batchInsert"> INSERT INTO user(name, age) VALUES <foreach collection="list" item="user" separator=","> (#{user.name}, #{user.age}) </foreach> </insert>
  1. 二级缓存

    • 谨慎使用,容易导致脏读
    • 适合读多写少的场景
  2. TypeHandler

    • 自定义复杂类型的处理
    • 如JSON字段、枚举转换等

6.4 日期处理经验

  1. 统一时区

    • 后端使用UTC时间存储
    • 前端根据用户时区显示
  2. API设计

    • 接收字符串参数(ISO格式)
    • 返回格式化的本地时间字符串
@GetMapping("/orders") public List<Order> getOrders( @RequestParam @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime start, @RequestParam @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime end) { // 业务逻辑 }
  1. 数据库索引
    • 为常用的时间查询字段添加索引
    • 避免在时间字段上使用函数

6.5 Stream 使用建议

  1. 避免过度使用

    • 简单操作直接使用循环
    • 复杂数据处理使用Stream
  2. 性能敏感场景

    • 使用基本类型流(IntStream等)
    • 避免在循环中创建Stream
  3. 可读性平衡

    • 过长的链式调用考虑拆分
    • 为复杂操作添加注释
// 好的示例:清晰表达数据处理流程 List<String> activeUserNames = users.stream() .filter(User::isActive) // 过滤活跃用户 .sorted(comparing(User::getAge)) // 按年龄排序 .map(User::getName) // 提取姓名 .collect(toList()); // 收集结果

7. 常见问题解决方案

在实际开发中,我遇到并解决了一些典型问题,以下是记录和解决方案:

7.1 定时任务不执行

问题现象

  • @Scheduled方法没有被调用
  • 没有错误日志

排查步骤

  1. 检查是否添加了@EnableScheduling
  2. 确认任务类被Spring管理(有@Component等注解)
  3. 检查Cron表达式格式是否正确
  4. 查看是否有未处理的异常导致线程终止

解决方案

@SpringBootApplication @EnableScheduling // 确保添加此注解 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

7.2 WebSocket 连接不稳定

问题现象

  • 连接频繁断开
  • 消息偶尔丢失

解决方案

  1. 实现心跳机制保持连接
  2. 添加自动重连逻辑
  3. 网络抖动处理
// 前端重连逻辑 let reconnectInterval = 1000; const maxReconnectInterval = 30000; function connect() { const ws = new WebSocket(url); ws.onclose = function() { setTimeout(connect, reconnectInterval); reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval); }; ws.onopen = function() { reconnectInterval = 1000; // 重置间隔 }; }

7.3 MyBatis 动态SQL错误

问题现象

  • XML解析错误
  • 条件判断不生效

常见原因

  1. 特殊符号未转义
  2. 条件判断错误
  3. 参数类型不匹配

解决方案

<!-- 使用CDATA包裹复杂SQL --> <select id="complexQuery"> <![CDATA[ SELECT * FROM table WHERE create_time > #{start} AND (status = 1 OR is_vip = 1) ]]> </select>

7.4 日期计算错误

问题现象

  • 跨月/跨年计算错误
  • 时区转换不正确

解决方案

// 正确的日期范围计算 public static List<LocalDate> getDateRange(LocalDate start, LocalDate end) { List<LocalDate> dates = new ArrayList<>(); LocalDate current = start; while (!current.isAfter(end)) { dates.add(current); current = current.plusDays(1); } return dates; }

7.5 Stream 性能问题

问题现象

  • 大数据量处理慢
  • 内存占用高

优化方案

  1. 使用基本类型流
  2. 并行处理
  3. 尽早过滤数据
// 优化后的流操作 double total = bigDataList.parallelStream() .filter(item -> item.isValid()) // 先过滤 .mapToDouble(Item::getValue) // 避免装箱 .sum();

8. 项目架构思考

在完成"苍穹外卖"项目后,我对后端架构有了更深的理解,分享一些架构层面的思考:

8.1 分层设计优化

传统三层架构

Controller -> Service -> Mapper

改进方向

  1. 添加DTO层隔离实体和接口
  2. 引入Manager层处理复杂业务组合
  3. 明确各层职责边界

8.2 模块化拆分

按业务功能拆分

- order-service - user-service - payment-service - delivery-service

共用组件

  • 认证中心
  • 消息中心
  • 文件服务

8.3 技术选型考量

WebSocket扩展方案

  1. 原生WebSocket:轻量级,适合简单场景
  2. SockJS:兼容性更好,支持降级
  3. STOMP:协议更完善,适合复杂场景

定时任务方案对比

方案优点缺点适用场景
@Scheduled简单易用功能简单,不支持分布式单机简单任务
Quartz功能强大配置复杂复杂调度需求
XXL-JOB分布式支持,有管理界面需要额外部署企业级调度系统
Elastic-Job分布式,弹性调度学习成本高大规模分布式任务

8.4 性能优化方向

  1. 数据库优化

    • 读写分离
    • 分库分表
    • 缓存策略
  2. 接口优化

    • 异步处理
    • 结果缓存
    • 数据压缩
  3. JVM优化

    • 合理设置堆大小
    • GC调优
    • 线程池配置

8.5 监控与告警

必备监控项

  1. WebSocket连接数
  2. 定时任务执行情况
  3. 接口响应时间
  4. 系统资源使用率

实现方案

// 使用Micrometer暴露指标 @Bean public MeterRegistryCustomizer<PrometheusMeterRegistry> metricsCommonTags() { return registry -> registry.config().commonTags("application", "takeaway-service"); } // 定时任务监控示例 @Scheduled(cron = "0 0 * * * ?") public void monitoredTask() { Timer.Sample sample = Timer.start(registry); try { // 业务逻辑 } finally { sample.stop(registry.timer("scheduled.task", "name", "hourlyJob")); } }

9. 代码质量保障

在项目开发过程中,我特别注重代码质量的保障,以下是一些实践:

9.1 单元测试策略

WebSocket测试

@SpringBootTest @WebAppConfiguration public class WebSocketTest { @Autowired private ServerEndpointExporter exporter; @Test public void testOnOpen() throws Exception { // 模拟WebSocket会话 Session session = new TestSession(); WebSocketServer endpoint = new WebSocketServer(); // 测试连接建立 endpoint.onOpen(session, "testUser"); assertTrue(WebSocketServer.containsUser("testUser")); } }

定时任务测试

@SpringBootTest public class ScheduledTaskTest { @Autowired private OrderTask orderTask; @MockBean private OrderMapper orderMapper; @Test public void testDeliveryTimeout() { // 准备测试数据 Order order = new Order(); order.setStatus(OrderStatus.DELIVERY_IN_PROGRESS); when(orderMapper.findByStatusAndOrderTimeBefore( any(), any())) .thenReturn(Collections.singletonList(order)); // 执行定时任务 orderTask.processDeliveryTimeout(); // 验证状态更新 assertEquals(OrderStatus.COMPLETED, order.getStatus()); verify(orderMapper).update(order); } }

9.2 代码审查要点

  1. 定时任务
    • 是否处理了异常?
    • 是否有必要的日志?
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/4 1:48:55

本地Node.js中转服务接入国产大模型实战

1. 项目概述&#xff1a;这不是“翻墙用Claude”&#xff0c;而是本地IDE里跑通国产大模型推理链的实操闭环你是不是也遇到过这些场景&#xff1a;在VS Code里写Python脚本&#xff0c;想让AI自动补全SQL查询逻辑&#xff0c;但官方Claude Code插件只认Anthropic自家API&#x…

作者头像 李华
网站建设 2026/7/4 1:48:52

Moltbot本地AI网关部署:Node.js+WSL2保姆级实战指南

1. 项目概述&#xff1a;从Clawdbot到Moltbot&#xff0c;一个本地化AI工具守护进程的落地实践Clawdbot这个名字最近在技术圈里悄悄淡出&#xff0c;取而代之的是Moltbot——不是品牌营销的更名&#xff0c;而是实打实的合规性调整。它本质上是一个运行在本地或私有VPS上的Node…

作者头像 李华
网站建设 2026/7/4 1:48:41

Linux部署SpringBoot项目实战:从systemd服务化到生产级日志治理

1. 为什么“Linux部署SpringBoot项目”不是个简单复制粘贴的事很多人第一次在Linux上部署SpringBoot项目&#xff0c;心里想的都是&#xff1a;“不就是把jar包传上去&#xff0c;然后java -jar启动一下吗&#xff1f;”我当年也是这么想的——直到凌晨三点还在排查一个“明明能…

作者头像 李华
网站建设 2026/7/4 1:47:39

find-skills:轻量级AI技能元数据发现工具实战指南

1. 这个“元Skill”到底是什么&#xff1f;先破除三个常见误解“24小时15.3K安装量稳坐王座&#xff01;老金愿称之为元Skill&#xff01;”——这句标题在技术圈刷屏时&#xff0c;我第一时间打开终端敲了npx find-skills --help&#xff0c;结果弹出的不是炫酷的AI技能面板&a…

作者头像 李华
网站建设 2026/7/4 1:45:23

UE引擎Shot命令详解:专业截图与批量处理技巧

1. UE引擎中的截图功能概述在虚幻引擎&#xff08;Unreal Engine&#xff09;的日常开发中&#xff0c;截图功能是每个开发者都需要掌握的基础技能。不同于常规的屏幕截图工具&#xff0c;UE内置的Shot命令提供了更专业的场景捕获能力&#xff0c;特别适合需要精确控制截图参数…

作者头像 李华
网站建设 2026/7/4 1:44:57

Pandas数据清洗实战:缺失值、异常值与重复数据处理

1. Pandas数据清洗实战概述数据清洗是数据分析过程中最基础也最关键的环节。在实际工作中&#xff0c;我们拿到的原始数据往往存在各种问题&#xff1a;缺失值、重复记录、异常数据、格式不一致等。这些问题如果不处理&#xff0c;会直接影响后续分析结果的准确性。Pandas作为P…

作者头像 李华