1. Spring Task 定时任务实战指南
定时任务是后端开发中常见的需求场景,Spring 提供了简单易用的@Scheduled注解来实现定时任务调度。下面我将结合实际项目经验,详细介绍 Spring Task 的使用方法和注意事项。
1.1 定时任务典型应用场景
在实际项目中,我们经常遇到需要定时执行的任务,以下是一些典型场景:
金融业务场景:
- 信用卡每月还款提醒(每月固定日期发送短信/邮件)
- 房贷每月还款提醒(每月指定日期扣款)
- 理财到期提醒(根据产品期限提前通知)
电商业务场景:
- 未支付订单超时取消(30分钟未支付自动取消)
- 发货超时提醒(商家超过48小时未发货提醒)
- 自动确认收货(发货后15天自动确认)
社交业务场景:
- 生日/纪念日提醒(提前1天发送祝福)
- 长期未登录用户召回(30天未登录发送召回邮件)
系统维护场景:
- 每日凌晨执行数据备份
- 每周清理临时文件
- 每月生成统计报表
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()); }); } } }关键点说明:
- 使用
@Component让 Spring 管理任务类 @Scheduled注解标记定时方法- 通过
orderMapper操作数据库 - 添加详细的日志记录
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 对比
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 单向请求-响应 | 全双工双向通信 |
| 连接建立 | 每次请求新建连接 | 一次握手持久连接 |
| 头部开销 | 每次请求完整头部 | 首次握手后仅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 > #{begin} </if> <if test="end != null"> AND order_time < #{end} </if> <if test="status != null"> AND status = #{status} </if> </where> </select>注意事项:
XML中的特殊符号需要使用转义:
<表示 <>表示 >&表示 &
<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>关键点:
<set>标签会智能处理后缀逗号<foreach>用于构建IN语句- 参数通过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/Shanghai5. 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();最佳实践:
- 数据量足够大(通常>10,000)才使用并行流
- 避免共享可变状态
- 考虑使用线程安全的收集器
// 线程安全收集 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 定时任务优化经验
任务执行时间选择:
- 避开业务高峰期(如外卖午高峰11:00-13:00)
- 数据统计类任务放在凌晨执行
- 短周期任务错开时间点(不要所有任务都在整点执行)
长任务处理:
- 添加
@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 性能优化
连接管理:
- 限制单个IP的最大连接数
- 实现心跳检测断开无效连接
消息压缩:
- 大消息使用gzip压缩
- 二进制协议比JSON更高效
集群支持:
- 使用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 优化建议
- 批量操作:
- 使用
<foreach>实现批量插入 - 开启
rewriteBatchedStatements提升性能
- 使用
<insert id="batchInsert"> INSERT INTO user(name, age) VALUES <foreach collection="list" item="user" separator=","> (#{user.name}, #{user.age}) </foreach> </insert>二级缓存:
- 谨慎使用,容易导致脏读
- 适合读多写少的场景
TypeHandler:
- 自定义复杂类型的处理
- 如JSON字段、枚举转换等
6.4 日期处理经验
统一时区:
- 后端使用UTC时间存储
- 前端根据用户时区显示
API设计:
- 接收字符串参数(ISO格式)
- 返回格式化的本地时间字符串
@GetMapping("/orders") public List<Order> getOrders( @RequestParam @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime start, @RequestParam @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime end) { // 业务逻辑 }- 数据库索引:
- 为常用的时间查询字段添加索引
- 避免在时间字段上使用函数
6.5 Stream 使用建议
避免过度使用:
- 简单操作直接使用循环
- 复杂数据处理使用Stream
性能敏感场景:
- 使用基本类型流(IntStream等)
- 避免在循环中创建Stream
可读性平衡:
- 过长的链式调用考虑拆分
- 为复杂操作添加注释
// 好的示例:清晰表达数据处理流程 List<String> activeUserNames = users.stream() .filter(User::isActive) // 过滤活跃用户 .sorted(comparing(User::getAge)) // 按年龄排序 .map(User::getName) // 提取姓名 .collect(toList()); // 收集结果7. 常见问题解决方案
在实际开发中,我遇到并解决了一些典型问题,以下是记录和解决方案:
7.1 定时任务不执行
问题现象:
@Scheduled方法没有被调用- 没有错误日志
排查步骤:
- 检查是否添加了
@EnableScheduling - 确认任务类被Spring管理(有
@Component等注解) - 检查Cron表达式格式是否正确
- 查看是否有未处理的异常导致线程终止
解决方案:
@SpringBootApplication @EnableScheduling // 确保添加此注解 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }7.2 WebSocket 连接不稳定
问题现象:
- 连接频繁断开
- 消息偶尔丢失
解决方案:
- 实现心跳机制保持连接
- 添加自动重连逻辑
- 网络抖动处理
// 前端重连逻辑 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解析错误
- 条件判断不生效
常见原因:
- 特殊符号未转义
- 条件判断错误
- 参数类型不匹配
解决方案:
<!-- 使用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 性能问题
问题现象:
- 大数据量处理慢
- 内存占用高
优化方案:
- 使用基本类型流
- 并行处理
- 尽早过滤数据
// 优化后的流操作 double total = bigDataList.parallelStream() .filter(item -> item.isValid()) // 先过滤 .mapToDouble(Item::getValue) // 避免装箱 .sum();8. 项目架构思考
在完成"苍穹外卖"项目后,我对后端架构有了更深的理解,分享一些架构层面的思考:
8.1 分层设计优化
传统三层架构:
Controller -> Service -> Mapper改进方向:
- 添加DTO层隔离实体和接口
- 引入Manager层处理复杂业务组合
- 明确各层职责边界
8.2 模块化拆分
按业务功能拆分:
- order-service - user-service - payment-service - delivery-service共用组件:
- 认证中心
- 消息中心
- 文件服务
8.3 技术选型考量
WebSocket扩展方案:
- 原生WebSocket:轻量级,适合简单场景
- SockJS:兼容性更好,支持降级
- STOMP:协议更完善,适合复杂场景
定时任务方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| @Scheduled | 简单易用 | 功能简单,不支持分布式 | 单机简单任务 |
| Quartz | 功能强大 | 配置复杂 | 复杂调度需求 |
| XXL-JOB | 分布式支持,有管理界面 | 需要额外部署 | 企业级调度系统 |
| Elastic-Job | 分布式,弹性调度 | 学习成本高 | 大规模分布式任务 |
8.4 性能优化方向
数据库优化:
- 读写分离
- 分库分表
- 缓存策略
接口优化:
- 异步处理
- 结果缓存
- 数据压缩
JVM优化:
- 合理设置堆大小
- GC调优
- 线程池配置
8.5 监控与告警
必备监控项:
- WebSocket连接数
- 定时任务执行情况
- 接口响应时间
- 系统资源使用率
实现方案:
// 使用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 代码审查要点
- 定时任务:
- 是否处理了异常?
- 是否有必要的日志?