《苍穹外卖》实战:从零到一构建高并发外卖系统核心笔记

张开发
2026/4/16 0:37:53 15 分钟阅读

分享文章

《苍穹外卖》实战:从零到一构建高并发外卖系统核心笔记
1. 公共字段自动填充的工程化实践第一次看到《苍穹外卖》项目里那些重复出现的创建人、创建时间、修改人、修改时间字段时我就意识到这绝对是个需要优化的地方。每个实体类都手动维护这些字段不仅容易出错后期维护更是噩梦。好在Spring AOP给我们提供了完美的解决方案。我采用的方案是自定义注解切面编程。首先定义了一个AutoFill注解标注在需要自动填充的Mapper方法上。然后创建了AutoFillAspect切面类通过Before通知在方法执行前自动注入这些字段值。这里有个小技巧我用了枚举来区分操作类型INSERT/UPDATE这样切面就能智能判断该填充哪些字段。Retention(RetentionPolicy.RUNTIME) Target(ElementType.METHOD) public interface AutoFill { OperationType value(); }实际使用时Service层完全不用关心这些字段只需要在Mapper方法加上注解就行。比如更新分类信息时AutoFill(OperationType.UPDATE) void update(Category category);但要注意两个坑第一SQL语句里这些字段还是得写第二字段赋值用的是反射性能会有轻微损耗。实测下来在常规业务场景中这点损耗完全可以接受。2. 微信生态集成实战心得做外卖系统免不了要对接微信生态这里我踩过的坑可能比写的代码还多。先说小程序登录流程前端获取code传给后端后端用codeappidsecret换openid。听起来简单但实际开发时微信接口的各种限制会让你怀疑人生。我专门封装了一个WeChatAuthService处理所有微信交互关键代码如下public String getOpenid(String code) { String url https://api.weixin.qq.com/sns/jscode2session; MapString, String params new HashMap(); params.put(appid, weChatProperties.getAppid()); params.put(secret, weChatProperties.getSecret()); params.put(js_code, code); params.put(grant_type, authorization_code); String response httpClient.doGet(url, params); return JSON.parseObject(response).getString(openid); }支付模块更是个深坑。没有商户资质的情况下我参考了网上的模拟支付方案。核心思路是在前端跳过真正的支付流程直接回调成功状态后端收到通知后自动更新订单状态。虽然不能真实交易但完整走通了支付流程对开发测试完全够用。3. Redis缓存策略的进阶玩法刚开始用Redis缓存菜品数据时我天真地以为简单set/get就完事了结果被缓存一致性问题狠狠教育。后来摸索出一套组合策略双写模式更新数据库后立即更新缓存失效模式数据变更时直接删除缓存延时双删先删缓存→更新DB→休眠→再删缓存在《苍穹外卖》中我最终采用了Spring Cache注解方案因为它足够简单高效Cacheable(value dishCache, key #categoryId) public ListDishVO list(Long categoryId) { // 查询数据库 } CacheEvict(value dishCache, allEntries true) public void save(DishDTO dishDTO) { // 保存逻辑 }特别提醒缓存key的设计非常重要。我习惯用业务前缀:参数的格式比如dishCache:1表示分类ID为1的菜品。这样既清晰又能避免key冲突。4. 高并发下的购物车设计购物车模块看似简单实则暗藏玄机。最大的挑战是要在用户体验和系统性能间找到平衡。我的方案是读写分离读操作走Redis缓存写操作同步到MySQL异步持久化用户添加商品时先写Redis通过消息队列异步落库冗余设计在购物车表中存储菜品名称、图片等冗余字段避免频繁联表查询核心数据结构设计如下# 用户购物车 user:cart:{userId} - { dish_1: 2, // 菜品1数量2 dish_2: 1 // 菜品2数量1 }实际编码时我抽象出了一个CartService统一处理购物车逻辑。其中有个精妙的设计是使用Redis的HASH结构存储购物车商品既能快速查询单个商品数量又能高效获取整个购物车。5. 订单系统的防重与幂等设计外卖系统的订单模块最怕两件事重复提交和支付掉单。我用了三个技术手段来防范前端防抖提交按钮点击后立即禁用防止连点Token机制页面加载时后端生成唯一token提交时校验数据库唯一索引对订单号字段建立唯一索引支付回调处理更要小心。我设计了状态机模式来管理订单状态流转public enum OrderStatus { PENDING_PAYMENT, // 待支付 PAID, // 已支付 COMPLETED, // 已完成 CANCELLED // 已取消 }每个状态变更都要记录操作日志这是后期排查问题的黄金数据。建议使用AOP统一记录代码大概长这样AfterReturning(execution(* com.sky.service.OrderService.*(..))) public void logOrderChange(JoinPoint jp) { // 获取方法参数中的订单ID // 记录变更前后的状态 // 存入日志表 }6. 性能优化的那些事儿项目上线前我用JMeter做了压力测试发现几个性能瓶颈N1查询问题获取订单详情时频繁查询关联表大对象序列化菜品列表JSON太大导致Redis阻塞锁竞争激烈秒杀活动时库存扣减超卖解决方案也很有意思。对于N1问题我用了MyBatis的collection标签做嵌套查询大对象序列化改用Protocol Buffers替代JSON库存扣减则采用RedisLua脚本实现原子操作local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then redis.call(DECR, KEYS[1]) return 1 else return 0 end还有个容易被忽视的优化点数据库连接池配置。经过多次调优最终确定的HikariCP参数如下spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle10 spring.datasource.hikari.idle-timeout30000 spring.datasource.hikari.connection-timeout20007. 异常处理的艺术好的异常处理能让系统稳定性提升好几个Level。我的异常处理哲学是业务异常给用户友好提示如菜品已售罄系统异常记录详细日志自动报警第三方异常设计重试机制和降级方案在Spring中我用ControllerAdvice统一处理异常ExceptionHandler(BusinessException.class) public ResponseEntityErrorResult handleBusinessEx(BusinessException ex) { ErrorResult error new ErrorResult(ex.getCode(), ex.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } ExceptionHandler(Exception.class) public ResponseEntityErrorResult handleUnexpectedEx(Exception ex) { log.error(System error, ex); ErrorResult error new ErrorResult(500, 系统繁忙请稍后再试); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); }对于微信支付等第三方调用一定要设置合理的超时时间和重试策略。我通常用Spring Retry来实现Retryable(value WeChatPayException.class, maxAttempts 3, backoff Backoff(delay 1000, multiplier 2)) public void callWeChatPay() { // 支付调用逻辑 }8. 监控与报警体系建设系统上线只是开始运维监控才是持久战。我搭建的监控体系包括业务指标订单量、支付成功率等系统指标CPU、内存、磁盘等中间件Redis命中率、MySQL慢查询使用PrometheusGrafana的方案关键配置如下# Prometheus配置示例 scrape_configs: - job_name: spring metrics_path: /actuator/prometheus static_configs: - targets: [localhost:8080]对于报警规则我建议遵循三个黄金指标错误率超过1%立即报警响应时间P99大于1秒需要关注吞吐量突然下降50%可能是故障前兆日志收集用的是ELK栈特别要注意日志格式统一。我定义的日志模板%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n9. 持续集成与交付项目后期我引入了完整的CI/CD流程。每次代码提交都会自动触发单元测试必须全部通过SonarQube代码质量检查Docker镜像构建测试环境部署GitLab CI的配置文件大概长这样stages: - test - build - deploy unit-test: stage: test script: - mvn test docker-build: stage: build script: - docker build -t sky-take-out . deploy-test: stage: deploy script: - kubectl apply -f k8s/deployment.yaml这套流程让我们的发布效率提升了70%以上。有个小技巧在Dockerfile中使用多阶段构建既能减小镜像体积又能保证安全性FROM maven:3.8-jdk-11 AS build COPY . . RUN mvn package FROM openjdk:11-jre-slim COPY --frombuild /target/sky-take-out.jar /app.jar ENTRYPOINT [java,-jar,/app.jar]10. 项目复盘与经验总结做完《苍穹外卖》这个项目最大的收获不是技术上的而是对工程思维的培养。有几个深刻体会文档的重要性初期偷懒没写文档后期联调时各种沟通成本爆炸接口设计原则坚持单一职责参数不超过3个代码评审文化好的CR能发现80%的潜在问题给后来者的建议数据库设计阶段多花点时间好的表结构能省去后期大量重构。我的检查清单包括是否有适当的索引字段类型是否合理是否考虑了分库分表可能性是否有完善的注释最后分享一个性能优化的小故事有次排查接口超时发现是MyBatis的日志级别设为DEBUG导致。所以记住生产环境一定要用INFO及以上级别。

更多文章