ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot外卖系统实战:从下单到实时推送的全链路搭建

SpringBoot外卖系统实战:从下单到实时推送的全链路搭建 简介本资源是一套完整的Spring Boot外卖点餐系统毕业设计项目面向计算机专业本科生及Java初学者聚焦电商类Web应用开发实战覆盖商品管理、用户下单、商家接单、骑手配送与后台管理等核心业务闭环。压缩包含638个文件总计25.24MB其中Java源码121个构成后端主干逻辑Vue前端页面79个实现多角色交互界面HTML/JS/CSS共171个支撑前台展示与动态交互JPG/GIF/PNG184个提供界面截图与流程图示另含SQL建表脚本、PPT答辩材料、论文全文及可执行bat启动脚本结构完整、开箱即用。目前已有133人学习下载读者可直接部署运行、对照论文理解设计思路、参考ER图与数据库表结构掌握数据建模方法并通过多角色模块代码如UserController、FileController等深入学习Spring Boot分层架构与权限控制实践。1. 为什么一个“SpringBoot外卖点餐系统”能成为Java初学者真正跑通的第一个全栈项目不是因为功能多炫酷而是它刚好卡在「够用、不超纲、有闭环」的黄金交点上用户扫码下单、商家接单出餐、骑手抢单配送——三端角色清晰业务流完整数据库就MySQL一张订单表几张关联表连Redis缓存都可选配前端用Vue或Thymeleaf都能跑后端SpringBootMyBatis-Plus两小时搭好骨架。我带过37个应届生做课设92%的人卡在“登录后跳转404”或“下单成功但数据库没写入”根本原因不是技术难而是没理清SpringBoot里自动配置、事务传播、RESTful路径映射这三道隐形门槛。这篇笔记不讲“SpringBoot是什么”只拆解从spring-boot-starter-web起步怎么把“用户点一份酸辣土豆丝加米饭”这个动作稳稳落到MySQL的order_info表里并让商家后台实时看到新订单——每一步命令、每个注解、每个YAML字段都对应一个真实翻车现场。适合正在写课程设计、毕设开题、或者想用一个可演示项目验证自己SpringBoot动手能力的开发者。2. 用SpringBoot 2.7.18 MyBatis-Plus 搭建最小可行骨架5个文件撑起下单主链路SpringBoot外卖系统最常被低估的是版本兼容性对开发节奏的碾压式影响。网上大量“SpringBoot外卖源码”用的是2.3.x甚至1.5.x而当前主流IDEA2023.3新建项目默认选3.x一跑就报NoSuchBeanDefinitionException: No qualifying bean of type com.baomidou.mybatisplus.extension.service.IService——这不是你代码错是MyBatis-Plus 3.5.x和SpringBoot 3.x的JDK17强绑定导致的。所以第一步必须锁死技术栈SpringBoot 2.7.18JDK8兼容终点、MyBatis-Plus 3.5.3.1、MySQL 5.7。下面这5个文件就是让“用户点击下单按钮→数据库插入一条order记录”跑通的最小集合删掉任何一个都会断链。2.1 创建实体类OrderInfo字段命名必须和数据库严格对齐// src/main/java/com/example/food/entity/OrderInfo.java import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.math.BigDecimal; import java.time.LocalDateTime; TableName(order_info) // 明确指定表名避免MP自动转下划线失败 public class OrderInfo { TableId(type IdType.ASSIGN_ID) // 使用雪花算法ID比AUTO更可靠 private String id; private String userId; // 用户ID非主键关联user表 private String shopId; // 店铺ID private BigDecimal amount; // 订单金额用BigDecimal防浮点误差 private Integer status; // 状态0待支付、1已支付、2已接单、3已完成 private LocalDateTime createTime; // LocalDateTime类型MP自动映射MySQL DATETIME // getter/setter省略Lombok可自动生成 }关键参数说明TableName(order_info)强制指定表名避免MP按驼峰规则转成orderinfoMySQL默认大小写敏感TableId(type IdType.ASSIGN_ID)不用数据库自增ID防止高并发下单时ID冲突雪花ID在应用层生成更可控LocalDateTimeSpringBoot 2.7原生支持无需额外配置JsonFormat前端传2024-06-15T10:30:00直接解析。2.2 编写Mapper接口用MyBatis-Plus的CRUD封装替代XML手写SQL// src/main/java/com/example/food/mapper/OrderInfoMapper.java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.food.entity.OrderInfo; import org.apache.ibatis.annotations.Mapper; Mapper // 必须加此注解否则Spring找不到Mapper Bean public interface OrderInfoMapper extends BaseMapperOrderInfo { // 继承BaseMapper后insert()、selectById()等方法自动注入 // 如需复杂查询再在此接口中定义方法MP会扫描同名XML或注解SQL }逻辑说明BaseMapper提供了17个通用CRUD方法如insert()、updateById()完全覆盖下单场景的INSERT操作。不需要写OrderInfoMapper.xml也不用在application.yml里配置mybatis-plus.mapper-locations——这是MyBatis-Plus 3.4的默认行为省去90%的XML配置。2.3 定义Service层用Transactional控制下单事务边界// src/main/java/com/example/food/service/impl/OrderServiceImpl.java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.food.entity.OrderInfo; import com.example.food.mapper.OrderInfoMapper; import com.example.food.service.OrderService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Service public class OrderServiceImpl extends ServiceImplOrderInfoMapper, OrderInfo implements OrderService { Override Transactional(rollbackFor Exception.class) // 关键下单失败必须回滚 public boolean createOrder(OrderInfo order) { // 1. 校验库存伪代码实际需查商品表 if (!checkStock(order.getShopId(), acid-potato)) { return false; } // 2. 插入订单 this.save(order); // 调用BaseMapper.insert() // 3. 扣减库存伪代码 deductStock(order.getShopId(), acid-potato, 1); return true; } private boolean checkStock(String shopId, String itemId) { // 实际应查goods表stock字段此处简化 return true; } private void deductStock(String shopId, String itemId, int count) { // 实际执行UPDATE goods SET stock stock - ? WHERE shop_id ? AND item_id ? } }参数说明Transactional(rollbackFor Exception.class)必须显式指定rollbackFor否则运行时异常如空指针会回滚但检查异常如SQLException不会——而MySQL连接超时抛的就是检查异常this.save(order)调用父类ServiceImpl的save方法底层仍是OrderInfoMapper.insert()但自动处理了主键填充雪花ID。2.4 编写ControllerRESTful路径与参数校验硬约束// src/main/java/com/example/food/controller/OrderController.java import com.example.food.entity.OrderInfo; import com.example.food.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.math.BigDecimal; RestController RequestMapping(/api/order) Validated // 开启方法级校验 public class OrderController { Autowired private OrderService orderService; PostMapping public ResponseEntityString create(Valid RequestBody OrderRequest request) { OrderInfo order new OrderInfo(); order.setUserId(request.getUserId()); order.setShopId(request.getShopId()); order.setAmount(request.getAmount()); order.setStatus(0); // 初始状态为待支付 order.setCreateTime(java.time.LocalDateTime.now()); boolean success orderService.createOrder(order); return success ? ResponseEntity.ok(下单成功) : ResponseEntity.badRequest().body(库存不足); } // 内部DTO用于接收JSON并校验 Validated public static class OrderRequest { NotBlank(message 用户ID不能为空) private String userId; NotBlank(message 店铺ID不能为空) private String shopId; NotNull(message 金额不能为空) private BigDecimal amount; // getter/setter public String getUserId() { return userId; } public void setUserId(String userId) { this.userId userId; } public String getShopId() { return shopId; } public void setShopId(String shopId) { this.shopId shopId; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount amount; } } }关键设计点ValidatedRequestBody OrderRequest让Spring Boot自动校验userId非空、amount非null失败时直接返回400 Bad Request 错误信息不用在Service里写if判断ResponseEntityString比ResponseBody更灵活可精确控制HTTP状态码如库存不足返回400而非200。2.5 配置application.yml3个必填字段决定数据库连得上连不上# src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/food_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8必须用cj包老驱动会报ClassNotFoundException mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL调试必备 global-config: db-config: id-type: assign_id # 全局主键策略与TableId(type IdType.ASSIGN_ID)呼应血泪经验serverTimezoneAsia/Shanghai不加此参数MySQL 5.7会报The server time zone value XXX is unrecognized本质是JDBC驱动时区解析失败log-impl: ...StdOutImpl开启SQL日志下单时能看到INSERT INTO order_info (...) VALUES (?, ?, ?, ?, ?)及参数值比打断点快10倍id-type: assign_id全局配置主键生成策略避免每个实体类都写TableId。3. 让订单状态实时刷新用WebSocket替代轮询的3步落地法外卖系统最让用户焦虑的不是下单慢而是“已支付”后一直卡在“待接单”——传统方案是前端每5秒发一次GET /api/order/status?idxxx100个用户同时刷后端瞬间扛不住。WebSocket才是真·实时推送的工业级解法但SpringBoot整合WebSocket有3个隐藏雷区ServerEndpoint注解在内嵌Tomcat下失效、MessageMapping需要额外引入Spring Messaging、跨域配置位置反直觉。下面用最简路径打通“用户下单→商家页面弹窗提示”。3.1 启用WebSocket支持并配置跨域一行代码解决CORS拦截// src/main/java/com/example/food/config/WebSocketConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注册处理器路径允许所有域名访问生产环境需限定origin registry.addHandler(orderWebSocketHandler(), /ws/order) .setAllowedOrigins(*); // 关键放行跨域否则浏览器报错 } Bean public OrderWebSocketHandler orderWebSocketHandler() { return new OrderWebSocketHandler(); } }注意setAllowedOrigins(*)仅用于开发生产环境必须写具体域名如https://admin.food.com否则存在安全风险。3.2 编写WebSocket处理器用ConcurrentHashMap管理在线商家连接// src/main/java/com/example/food/websocket/OrderWebSocketHandler.java import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class OrderWebSocketHandler extends TextWebSocketHandler { // keyshopId, valueWebSocketSession列表一家店可能多个管理员登录 private static final MapString, WebSocketSession shopSessions new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String shopId extractShopId(session.getUri().getQuery()); // 从URL参数取shopId if (shopId ! null !shopId.isEmpty()) { shopSessions.put(shopId, session); } } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 商家可发消息给服务端如“已接单”此处简化不实现 } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { // 连接关闭时清理session shopSessions.values().remove(session); } // 提供给Service层调用推送订单消息 public static void sendMessageToShop(String shopId, String message) { WebSocketSession session shopSessions.get(shopId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 连接已断开移除无效session shopSessions.remove(shopId); } } } private String extractShopId(String query) { if (query null) return null; for (String param : query.split()) { if (param.startsWith(shopId)) { return param.substring(7); } } return null; } }关键逻辑ConcurrentHashMap保证多线程安全避免商家A接单时商家B的session被误删session.isOpen()检查连接有效性不检查直接send会抛IllegalStateException: The session is invalidextractShopId()从URL参数解析shopId前端连接时用new WebSocket(ws://localhost:8080/ws/order?shopIdshop_001)。3.3 在OrderService中触发推送下单成功后立即通知对应商家// 修改OrderServiceImpl.createOrder()方法 Override Transactional(rollbackFor Exception.class) public boolean createOrder(OrderInfo order) { if (!checkStock(order.getShopId(), acid-potato)) { return false; } this.save(order); deductStock(order.getShopId(), acid-potato, 1); // 新增推送订单消息给商家 String msg String.format({\orderId\:\%s\,\userId\:\%s\,\amount\:%.2f}, order.getId(), order.getUserId(), order.getAmount().doubleValue()); OrderWebSocketHandler.sendMessageToShop(order.getShopId(), msg); return true; }参数说明推送JSON格式字符串前端用JSON.parse()解析字段名与后端保持一致%.2f确保金额小数位数统一避免JavaScript浮点计算误差。3.4 前端JavaScript监听WebSocket5行代码实现无感刷新!-- 商家后台HTML片段 -- script let socket; const shopId shop_001; // 从登录态获取 function connectWebSocket() { socket new WebSocket(ws://localhost:8080/ws/order?shopId${shopId}); socket.onmessage function(event) { const data JSON.parse(event.data); alert(新订单${data.orderId}用户${data.userId}金额¥${data.amount}); // 此处可调用Vue的this.$message或更新订单列表 }; socket.onclose function() { console.log(WebSocket已断开3秒后重连); setTimeout(connectWebSocket, 3000); }; } connectWebSocket(); /script避坑提示onclose事件必须加自动重连否则网络抖动一次商家就收不到后续订单event.data是字符串必须JSON.parse()才能当对象用直接event.data.orderId会是undefined。4. 避坑SpringBoot外卖系统上线前必须排查的5个高频故障点新手在本地跑通后一部署到Linux服务器就崩90%的问题集中在以下5个点。这些不是“可能出错”而是我帮学生远程调试时平均每3个项目就会撞见1次的真实翻车现场按现象→原因→解决三步给出可执行方案。4.1 现象下单成功但MySQL里order_info表为空日志显示JDBC Connection closed原因SpringBoot默认HikariCP连接池最大连接数为10而外卖系统测试时100个并发请求瞬间打满连接池后续请求因拿不到连接直接失败且不报错静默丢弃。解决在application.yml中显式增大连接池配置spring: datasource: hikari: maximum-pool-size: 20 # 默认10调至20应对压测 connection-timeout: 30000 # 连接超时30秒避免卡死 idle-timeout: 600000 # 空闲连接600秒后释放 max-lifetime: 1800000 # 连接最长存活30分钟防MySQL主动断连验证方法启动后访问http://localhost:8080/actuator/metrics/hikaricp.connections.active看活跃连接数是否随请求上升。4.2 现象商家后台WebSocket连接频繁断开Chrome控制台报WebSocket is already in CLOSING or CLOSED state原因Nginx反向代理默认60秒超时WebSocket长连接被强制切断。解决修改Nginx配置/etc/nginx/conf.d/food.conflocation /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; # 关键透传Upgrade头 proxy_set_header Connection upgrade; # 关键透传Connection头 proxy_read_timeout 300; # 将超时从60秒延长到300秒 }注意改完执行sudo nginx -t sudo systemctl reload nginx否则配置不生效。4.3 现象用户上传头像后图片路径存进数据库是/upload/avatar_123.jpg但浏览器访问http://domain.com/upload/avatar_123.jpg404原因SpringBoot内嵌Tomcat不提供静态资源服务/upload/目录不在static或resources下无法被HTTP直接访问。解决在配置类中添加资源映射// src/main/java/com/example/food/config/WebConfig.java import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 将/upload/路径映射到服务器绝对路径 registry.addResourceHandler(/upload/**) .addResourceLocations(file:/home/food/upload/); // Linux路径 // Windows下改为 file:C:/food/upload/ } }安全提示file:协议路径必须是绝对路径相对路径file:upload/会导致java.nio.file.InvalidPathException。4.4 现象支付宝回调验签失败日志显示Invalid signature原因支付宝SDK要求验签时使用原始未解码的notify_url参数字符串但SpringBoot的RequestParam会自动URLDecode把变成空格、%2B变成导致签名原文不一致。解决用HttpServletRequest原始参数拼接PostMapping(/alipay/notify) public String alipayNotify(HttpServletRequest request) { MapString, String[] paramMap request.getParameterMap(); StringBuilder signSource new StringBuilder(); paramMap.forEach((key, values) - { if (!sign.equals(key) !sign_type.equals(key)) { // 按支付宝要求keyvalue拼接且value不URLDecode String value values[0]; // 直接取原始字符串不经过RequestParam解码 signSource.append(key).append().append(value).append(); } }); // 移除末尾再验签... }关键点request.getParameterMap()返回的是未经解码的原始参数RequestParam是解码后的此处必须用前者。4.5 现象Linux服务器部署后定时任务Scheduled(fixedRate 60000)不执行日志无任何输出原因SpringBoot默认不启用定时任务EnableScheduling注解漏加且Linux服务器时区为UTC而Scheduled按JVM时区解析时间。解决在启动类上加注解SpringBootApplication EnableScheduling // 必加否则所有Scheduled方法静默失效 public class FoodApplication { public static void main(String[] args) { SpringApplication.run(FoodApplication.class, args); } }启动时指定JVM时区java -Duser.timezoneGMT8 -jar food.jar或在application.yml中配置spring: jackson: time-zone: GMT8验证方法启动后查看日志是否有Started FoodApplication in X seconds之后每分钟应出现定时任务日志。5. 让答辩PPT脱颖而出3个可演示、可截图、可解释的技术亮点设计毕设答辩最怕评委问“你这个系统有什么技术深度”答“用了SpringBoot”等于没说。我指导过的21个学生里最终拿到优秀答辩的全靠在PPT里放了这3个可现场演示的细节——不是堆砌技术名词而是让评委亲手点几下鼠标就能验证你确实搞懂了原理。下面每个亮点都附带可执行代码、截图位置和话术脚本。5.1 亮点一订单号生成规则可视化——用雪花ID业务编码组合杜绝重复与泄露纯雪花ID如1823456789012345678对业务不友好看不出归属店铺、无法校验合法性。我在订单号里嵌入店铺编码日期序列号格式为SHOP001-20240615-00001既可读又防刷单。关键是用Redis原子计数器保证同一天同一店铺的序列号不重复比数据库自增更轻量。// OrderService.createOrder()中替换ID生成逻辑 public String generateOrderNo(String shopId) { String dateStr LocalDate.now().toString().replace(-, ); // 20240615 String key order:seq: shopId : dateStr; // Redis INCR命令原子递增返回值即序列号 Long seq redisTemplate.opsForValue().increment(key, 1); // 设置过期时间避免key永久占用内存 redisTemplate.expire(key, Duration.ofDays(7)); return String.format(%s-%s-%05d, shopId.toUpperCase(), dateStr, seq); }PPT演示脚本“请看这张截图指向Redis CLI执行GET order:seq:SHOP001:20240615的结果当前序列号是127说明今天这家店已接127单而数据库里订单号SHOP001-20240615-00127一眼就能定位到这条记录——这比UUID或纯数字ID更符合外卖业务场景。”5.2 亮点二商家接单状态机——用枚举状态流转校验避免脏数据订单状态从“待支付”→“已支付”→“已接单”→“已完成”传统做法是if(status0) status1极易出现“已接单”后又点“待支付”这种非法操作。我用状态机模式定义每个状态的合法后继状态接单时先校验再更新。// src/main/java/com/example/food/enums/OrderStatus.java public enum OrderStatus { WAIT_PAY(0, Set.of(1)), // 待支付只能转已支付 PAID(1, Set.of(2)), // 已支付只能转已接单 ACCEPTED(2, Set.of(3)), // 已接单只能转已完成 COMPLETED(3, Set.of()); // 已完成不可再变 private final int code; private final SetInteger nextStatuses; OrderStatus(int code, SetInteger nextStatuses) { this.code code; this.nextStatuses nextStatuses; } public boolean canTransitionTo(int targetStatus) { return nextStatuses.contains(targetStatus); } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.code code) return status; } throw new IllegalArgumentException(Unknown status code: code); } }// OrderService.acceptOrder()方法 public boolean acceptOrder(String orderId) { OrderInfo order orderMapper.selectById(orderId); OrderStatus current OrderStatus.fromCode(order.getStatus()); // 校验当前状态是否允许转为“已接单” if (!current.canTransitionTo(OrderStatus.ACCEPTED.code)) { throw new IllegalStateException( String.format(订单%s状态非法当前%d不允许转为%d, orderId, order.getStatus(), OrderStatus.ACCEPTED.code) ); } order.setStatus(OrderStatus.ACCEPTED.code); orderMapper.updateById(order); return true; }PPT演示脚本“评委老师可以现场测试我用Postman发一个PUT /api/order/accept?idxxx请求如果订单当前是‘已完成’状态系统会立刻返回500错误并提示‘状态非法’——这说明状态流转被代码级锁定不是靠程序员自觉而是架构强制保障。”5.3 亮点三订单超时自动关单——用DelayQueue实现精准定时不依赖数据库轮询传统方案用Scheduled每分钟扫一遍status0 and create_time now()-30min效率低且不准。我用JDK原生DelayQueue下单时创建一个OrderDelayTask放入队列30分钟后自动触发关单逻辑毫秒级精度零数据库压力。// src/main/java/com/example/food/task/OrderDelayTask.java import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class OrderDelayTask implements Delayed { private final String orderId; private final long triggerTime; // 触发时间戳毫秒 public OrderDelayTask(String orderId, long delayMinutes) { this.orderId orderId; this.triggerTime System.currentTimeMillis() TimeUnit.MINUTES.toMillis(delayMinutes); } Override public long getDelay(TimeUnit unit) { return unit.convert(triggerTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } Override public int compareTo(Delayed o) { return Long.compare(this.triggerTime, ((OrderDelayTask) o).triggerTime); } public String getOrderId() { return orderId; } }// 启动时初始化DelayQueue消费者线程 Component public class OrderTimeoutScheduler { private final DelayQueueOrderDelayTask delayQueue new DelayQueue(); private final OrderService orderService; public OrderTimeoutScheduler(OrderService orderService) { this.orderService orderService; // 启动守护线程消费队列 new Thread(this::consumeDelayQueue, OrderTimeoutThread).start(); } private void consumeDelayQueue() { while (!Thread.currentThread().isInterrupted()) { try { OrderDelayTask task delayQueue.take(); // 阻塞直到到期 orderService.closeTimeoutOrder(task.getOrderId()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } // 下单时调用此方法将任务加入队列 public void scheduleClose(String orderId) { delayQueue.offer(new OrderDelayTask(orderId, 30)); // 30分钟后关单 } }PPT演示脚本“这是DelayQueue的实时监控截图指向控制台打印的OrderTimeoutThread consumed task for order_abc当用户下单后系统立刻把关单任务塞进内存队列30分钟整准时触发——没有数据库扫描没有定时任务调度延迟这就是JDK并发包给我们的确定性。”我带学生做毕设时反复强调答辩不是讲你用了什么而是证明你理解为什么用这个、不用那个。比如WebSocket那页PPT我会特意对比轮询方案的QPS压测数据100并发下CPU 92% vs WebSocket下CPU 18%让评委看到技术选型背后的量化思考。希望帮到你。本文还有配套的精品资源点击获取
返回列表