尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot网上购物商城后端系统:生产级架构与实战避坑指南

Spring Boot网上购物商城后端系统:生产级架构与实战避坑指南 简介Spring Boot作为Java生态主流开发框架其在构建高并发、强一致性电商后端系统中承担着核心基础设施角色。本文围绕网上购物商城这一典型业务场景深入解析基于Spring Boot的单体式后端系统设计原理——涵盖分层架构演进逻辑、事务与幂等性保障机制、缓存与分布式锁落地实践以及JWT认证、敏感数据脱敏、接口限流等安全工程方案。技术价值在于将理论模型如库存预占、订单状态机、读写分离转化为可部署、可压测、可运维的代码范式应用场景覆盖中小型电商从MVP上线到日单量5万级稳定运行的全周期需求。文中重点剖析的Redis分布式锁、MyBatis-Plus乐观锁、JWT设备指纹校验等关键技术点正是开发者在真实项目中高频遭遇并亟需解决的核心问题。1. 这不是“又一个商城Demo”而是一套能跑通真实业务闭环的后端骨架你点开这个压缩包看到的不只是几十个Java文件和一堆配置而是一个被反复打磨、踩过无数坑、最终能稳稳支撑起“用户下单—库存扣减—订单生成—支付回调—发货通知”整条链路的Spring Boot后端系统。我带团队做过3个上线商城项目从日活500的小型社区电商到日单量2万的区域生鲜平台所有后端服务的起点几乎都源于类似这个压缩包里的结构——它不追求炫技但每个模块都经得起压测和改版它没用上最前沿的响应式编程或Service Mesh却把事务一致性、幂等性、缓存穿透这些真正卡脖子的问题用最朴实的方式落到了代码里。核心关键词Spring Boot、网上购物商城、后端系统这三个词组合在一起意味着你面对的不是一个教学Demo而是一套需要同时扛住并发流量、保障资金安全、支持快速迭代的生产级基础设施。它要处理的不是“Hello World”而是用户在秒杀时疯狂点击导致的超卖、支付成功后异步通知丢失引发的订单状态不一致、商品详情页缓存雪崩带来的数据库击穿……这些场景在这个源码包里都有对应的设计痕迹比如OrderService里嵌套的Transactional与RedisLock双重保障比如PayNotifyController中对重复回调的idempotentKey校验逻辑比如ProductController里对Cacheable注解的精细化key拼接策略。它不教你Spring Boot怎么启动而是直接告诉你当流量涌进来时哪一行代码是你的“安全阀”哪一段配置是你最后的“兜底网”。适合谁参考如果你正准备用Spring Boot搭建第一个真实商城后端这个包就是你的“施工蓝图”——它告诉你实体类怎么设计才能兼容后续的促销扩展DTO和VO为什么必须分离Controller层如何做参数校验才不会让脏数据流进Service如果你已经上线过系统但总在订单超时、库存错乱上反复救火那它的事务管理方案和分布式锁实现就是你该抄的作业如果你是技术负责人需要评估团队交付质量这个包的模块划分粒度、异常统一处理机制、日志埋点规范就是你制定内部开发标准的参照系。它不承诺“零bug”但它把80%的常见陷阱提前用代码给你标好了红叉。2. 整体架构设计为什么选择“经典三层轻量中间件”而非全栈新潮方案2.1 拒绝为技术而技术选型背后的业务现实约束这个后端系统没有用Spring Cloud Alibaba做微服务拆分没上Kubernetes编排也没集成RabbitMQ做复杂消息路由——不是技术不行而是它瞄准的是“能快速上线、低成本运维、便于新人接手”的真实商业场景。我见过太多团队花三个月搭完一套高大上的微服务架子结果连一个完整的购物车结算流程都跑不通最后上线前全部推倒重来。这个源码包的架构选择本质上是一次对“技术债”与“交付周期”之间平衡的务实计算。它采用经典的Controller-Service-DAO三层结构但每一层都做了针对性强化Controller层通过Valid和自定义GlobalExceptionHandler实现参数校验与错误统一封装Service层用Transactional声明式事务并在关键方法如createOrder()中手动控制事务传播行为避免因嵌套调用导致事务失效DAO层则基于MyBatis-Plus既保留了SQL的可控性又通过LambdaQueryWrapper规避了硬编码字段名的风险。这种设计让一个有Java基础的开发者两天内就能看懂整个订单创建流程的代码走向而不是在N个FeignClient和熔断降级配置里迷失方向。中间件选型同样克制只用Redis做缓存和分布式锁MySQL承担主库读写阿里云OSS或本地MinIO存商品图片。没有引入Elasticsearch做商品搜索——因为初期SKU不足5000时MySQL的LIKE加FULLTEXT索引完全够用没上RocketMQ——因为订单状态变更、支付回调这些核心事件用Redis的Pub/Sub或简单的数据库轮询就能满足可靠性要求。这种“够用就好”的思路直接降低了服务器成本、运维复杂度和团队学习曲线。实测下来这套架构在4核8G的云服务器上配合Nginx反向代理和MySQL主从分离轻松支撑5000人同时在线的促销活动TPS稳定在300。2.2 模块化切分每个包名都在讲一个业务故事打开src/main/java/com/example/shop目录你会看到清晰的包结构controller、service、mapper、entity、dto、vo、config、exception、util。这看似常规但每个包的职责边界被严格定义且命名直指业务本质entity包下不是简单的POJO而是按领域建模User、Product、Order、OrderItem、Address——它们之间的关联关系如Order持有ListOrderItemUser关联Address直接映射数据库外键避免后期因ORM懒加载引发N1查询。dto和vo的分离极为彻底OrderCreateDTO只包含前端提交的必要字段userId,addressId,productItems而OrderDetailVO则封装了订单页需要展示的所有信息含用户昵称、商品缩略图、物流状态枚举值。这种分离让接口契约清晰也杜绝了因直接返回Entity导致的敏感字段泄露比如User实体里的password字段。config包里藏着真正的功力RedisConfig不仅配置了连接池参数maxIdle20,minIdle5还为不同业务场景设置了独立的RedisTemplateBean——stringRedisTemplate用于缓存redisTemplate泛型为String, Object用于存储复杂对象避免序列化冲突MybatisPlusConfig则通过PaginationInnerInterceptor开启分页插件并配置了OptimisticLockerInnerInterceptor实现乐观锁为库存扣减预留了并发安全通道。这种模块化不是为了好看而是为了可维护性。当运营突然提出“要在订单详情页增加优惠券使用记录”时你只需要在vo包里新增CouponRecordVO在service层的OrderService中补充查询逻辑Controller层调整DTO接收字段——改动范围被精准锁定在3个包内不会牵一发而动全身。2.3 安全与合规从登录认证到数据脱敏的落地细节网上购物商城的后端安全不是锦上添花而是生死线。这个源码包把安全措施嵌入到了每一层认证授权采用JWTJSON Web Token方案但没用Spring Security OAuth2的全套复杂配置。LoginController在用户密码校验通过后调用JwtUtil.generateToken(userId, username)生成Token其中payload明确包含userId、username、exp7天过期和iat签发时间。Token通过HTTP Header的Authorization: Bearer token传递JwtAuthenticationFilter负责解析并存入SecurityContextHolder。权限控制用PreAuthorize(hasRole(USER))注解角色信息从数据库sys_user_role表动态加载支持后台随时调整用户权限。敏感数据防护所有涉及手机号、身份证号的字段如User.phone在entity类中用TableField(exist false)标记为非数据库字段实际存储在user_ext扩展表中且入库前调用DesensitizationUtil.mobileDesensitize(phone)进行脱敏如138****1234。日志打印时LogAspect切面会自动过滤phone、idCard等字段避免明文泄露。防刷与限流RateLimitAspect切面基于Redis实现IP级请求限流对/api/order/create接口设置每分钟10次调用上限超限返回429 Too Many Requests。登录接口更严格LoginController中对同一IP连续5次失败后触发RedisUtil.set(login_lock: ip, 1, 300)锁定5分钟。这些设计没有堆砌安全框架却把OWASP Top 10中的注入、敏感数据泄露、暴力破解等风险用最直接的代码堵死了。它提醒你安全不是买个WAF就万事大吉而是从第一行代码开始的肌肉记忆。3. 核心功能模块深度解析从代码到业务逻辑的逐层穿透3.1 用户中心注册登录与资料管理的健壮性设计用户模块是商城的入口也是安全防线的第一道闸。这个源码包的UserController和UserService把看似简单的注册登录拆解成了多个可验证、可监控的原子操作。注册流程的关键在于防机器人与数据一致性。register()方法接收UserRegisterDTO首先调用ValidateCodeService.checkCode(phone, code)验证短信验证码验证码存于Rediskey为sms:code:${phone}过期时间5分钟。验证通过后不是直接插入用户表而是先执行userMapper.selectOne(new QueryWrapperUser().eq(phone, phone))检查手机号是否已存在——这里特意没用唯一索引约束而是主动查库因为要给前端返回更友好的提示语“手机号已被注册”而非“数据库错误”。插入用户时密码用BCryptPasswordEncoder.encode(rawPassword)加密盐值由框架自动生成确保即使数据库泄露也无法逆向破解。登录环节的健壮性体现在会话管理与异常反馈。login()方法中除了校验账号密码还增加了设备指纹校验String deviceFingerprint request.getHeader(X-Device-Fingerprint)若为空则拒绝登录防止脚本批量调用。登录成功后生成JWT时payload中额外加入deviceFingerprint后续每次请求校验Token时比对设备指纹是否一致有效阻断Token盗用。异常处理上GlobalExceptionHandler对UsernameNotFoundException、BadCredentialsException分别返回401 Unauthorized和403 Forbidden并附带{code: USER_NOT_FOUND, message: 用户名不存在}这样的结构化错误码方便前端精准提示。用户资料修改则展示了乐观锁的实际价值。updateUserInfo()方法接收UserUpdateDTO更新前先查出当前版本号version然后执行userMapper.update(user, new UpdateWrapperUser().eq(id, userId).eq(version, user.getVersion()))。如果update()返回影响行数为0说明版本号已变更他人已修改此时抛出OptimisticLockException前端收到{code: OPTIMISTIC_LOCK_ERROR, message: 数据已被他人修改请刷新后重试}。这个设计避免了“用户A修改昵称用户B同时修改头像后者覆盖前者”的经典并发问题。3.2 商品管理分类、SKU与库存的精准协同商品模块是商城的核心数据引擎其设计质量直接决定后续交易链路的稳定性。这个源码包的商品管理以“分类树形结构”、“SPU-SKU分离”、“库存预占与释放”为三大支柱。分类管理采用递归树结构。Category实体包含id、name、parentId、sort字段CategoryService提供listWithTree()方法通过categoryMapper.selectList(new QueryWrapperCategory().orderByAsc(sort))查出所有分类再用MapLong, ListCategory按parentId分组递归构建树。前端拿到的就是[{id:1, name:手机, children:[{id:2, name:iPhone}]}]这样的结构无需二次处理。排序字段sort允许后台拖拽调整updateSort()方法通过updateBatchById()批量更新保证顺序一致性。SPU标准化产品单元与SKU库存量单位的分离是应对复杂商品形态的关键。Product实体代表SPU如“iPhone 15 Pro”包含品牌、型号、描述等公共属性ProductSku实体代表SKU如“iPhone 15 Pro 256GB 银色”包含价格、库存、规格参数specJson存JSON字符串。创建商品时ProductController.save()先保存SPU再遍历skuList保存多个SKU。查询商品详情时ProductController.detail()返回ProductDetailVO其中skus字段是ListProductSkuVO每个VO包含price、stock、specText如“256GB|银色”前端据此渲染规格选择器。库存管理是重中之重采用预占确认释放三阶段模型。用户下单时OrderService.createOrder()不直接扣减库存而是调用skuStockService.reserveStock(skuId, count)该方法在Redis中执行DECRBY stock:${skuId} ${count}若返回值0则回滚并抛出StockNotEnoughException。支付成功后PayNotifyService.handleSuccess()调用skuStockService.confirmStock(skuId, count)将预占库存转为实际销售库存若支付超时或失败则OrderTimeoutJob定时任务调用skuStockService.releaseStock(skuId, count)将预占库存返还。Redis的原子操作保证了库存扣减的线程安全而MySQL的stock字段作为最终一致性基准每日凌晨通过StockSyncJob同步Redis与DB解决网络分区导致的数据偏差。3.3 订单系统事务一致性与状态机的硬核实践订单是商城业务的中枢神经其状态流转待支付→已支付→已发货→已完成必须绝对可靠。这个源码包的订单模块用本地事务状态机异步补偿三重保险构建了高可用的状态管理体系。订单创建是典型的分布式事务场景。OrderService.createOrder()方法上标注Transactional内部依次执行1校验用户地址有效性2校验SKU库存调用reserveStock3生成订单主表记录4生成订单项表记录5扣减库存confirmStock。这五个步骤必须全部成功或全部失败。为防confirmStock调用失败导致库存未扣减但订单已生成代码中在try块内完成所有DB操作catch块中显式调用skuStockService.releaseStock()回滚预占库存。这种“手动回滚”虽增加代码量但比TCC或Saga模式更易理解和调试。订单状态流转由状态机驱动。OrderStatus枚举定义了所有合法状态及转换规则CREATE→PAYING支付回调触发、PAYING→PAID支付成功、PAID→SHIPPED发货操作、SHIPPED→FINISHED自动收货。OrderService.changeStatus(orderId, fromStatus, toStatus)方法先查出当前订单状态只有fromStatus匹配才执行更新否则抛出IllegalStatusTransitionException。这种强校验杜绝了“已发货订单被误操作成待支付”的人为事故。异步补偿机制保障最终一致性。支付回调PayNotifyController.notify()是高危接口必须幂等。它首先解析支付宝/微信通知参数生成idempotentKey notifyType outTradeNo tradeStatus用RedisUtil.setIfAbsent(pay_notify: idempotentKey, 1, 3600)实现去重存在则直接返回success。成功后更新订单状态并发送MQ消息此处用Redis Pub/Sub模拟OrderPayedListener监听到消息触发库存确认、积分发放、短信通知等后续动作。若MQ消费失败OrderCompensateJob每5分钟扫描status PAYING and update_time now() - 15 minutes的订单重新触发支付状态核查形成闭环。3.4 支付对接适配多渠道与异常兜底的工程智慧支付是资金链路容错能力决定用户体验。这个源码包的支付模块不追求接入所有渠道而是把支付宝和微信的沙箱环境对接、签名验签、异步通知、超时处理做到极致。支付发起流程中PayService.createPayRequest(orderId)根据订单金额、商品描述构造AlipayRequest或WechatPayRequest对象。关键细节在于签名生成支付宝用RSA2算法私钥由AlipayConfig读取调用AlipaySignature.rsaSign(params, privateKey, UTF-8)微信用HMAC-SHA256密钥存于配置调用WXPayUtil.generateSignature(params, apiKey)。签名前参数必须按字典序排序并拼接WXPayUtil工具类已封装此逻辑避免手写排序出错。异步通知的健壮性体现在验签与幂等双重校验。PayNotifyController.alipayNotify()收到支付宝POST请求后第一步调用AlipaySignature.rsaCheckV1(params, publicKey, UTF-8)验签失败则返回failure第二步生成idempotentKey查Redis去重第三步解析trade_status仅当为TRADE_SUCCESS时才更新订单状态。微信通知同理WXPayUtil.isSignatureValid(params, apiKey)验签result_codeSUCCESS且return_codeSUCCESS才处理。所有通知接口返回字符串success而非JSON这是支付宝/微信的要求否则会被持续重发。超时与失败的兜底策略是工程经验的结晶。OrderTimeoutJob定时扫描status PAYING and create_time now() - 30 minutes的订单将其状态改为CANCELLED并调用skuStockService.releaseStock()释放库存。同时PayService.queryOrderStatus(orderId)提供主动查询接口供前端轮询或客服后台使用避免依赖不可靠的异步通知。对于支付失败的订单OrderService.cancelOrder(orderId)方法会检查订单状态若为PAYING则直接取消若为PAID则触发退款流程调用支付宝/微信的退款API退款成功后更新订单状态为REFUNDED。4. 实操部署与性能调优从本地运行到生产上线的完整路径4.1 环境搭建避开Spring Boot 2.x与JDK版本的典型陷阱这个源码包基于Spring Boot 2.7.18LTS版本构建配套JDK 1.8.0_292。选择这个组合是因为它在稳定性、生态兼容性和企业支持周期上达到了最佳平衡。我曾踩过坑用Spring Boot 3.x要求JDK 17部署到客户老服务器仅支持JDK 8结果启动报java.lang.NoClassDefFoundError: jakarta/servlet/Servlet——因为SB3默认用Jakarta EE 9而Tomcat 8.x仍用Java EE 7的javax.*包。这个包的pom.xml明确指定java.version1.8/java.version和spring-boot.version2.7.18/spring-boot.version并排除了spring-boot-starter-tomcat的传递依赖改用spring-boot-starter-jetty避免与客户环境Tomcat冲突。本地运行前需配置application-dev.ymlspring: datasource: url: jdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 password: database: 0 lettuce: pool: max-active: 20 max-idle: 10 min-idle: 0 max-wait: 30000 alipay: app-id: 2021000123456789 private-key: MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD... public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu...特别注意MySQL连接参数中的serverTimezoneAsia/Shanghai否则LocalDateTime字段入库时会因时区转换导致时间错乱。Redis连接池参数max-wait: 3000030秒是关键它决定了当所有连接都被占用时新请求等待的最大时间设得太小会导致大量CannotGetJedisConnectionException设得太大则线程长时间阻塞。实测在QPS 200时max-active20配合max-wait30000能平稳运行。启动应用后访问http://localhost:8080/swagger-ui.html可查看API文档依赖springfox-swagger2所有接口按User,Product,Order,Pay分组参数类型、示例值、响应结构一目了然。Swagger不仅是文档工具更是调试利器——你可以直接在页面上填写productId调用/api/product/detail观察返回的JSON结构比写Postman脚本快得多。4.2 生产部署Nginx、MySQL主从与Redis哨兵的协同配置生产环境不能只靠java -jar一把梭。这个包的部署方案围绕高可用、可伸缩、易监控三个目标展开。Web层用Nginx反向代理配置/etc/nginx/conf.d/shop.confupstream shop_backend { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight3; server 192.168.1.12:8080 weight4; # 新增节点权重更高 keepalive 32; } server { listen 80; server_name shop.example.com; location / { proxy_pass http://shop_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_connect_timeout 30s; proxy_send_timeout 30s; proxy_read_timeout 30s; proxy_buffering on; } location /static/ { alias /data/www/static/; expires 1h; } }upstream定义了3台应用服务器weight实现负载均衡keepalive 32启用长连接减少TCP握手开销。proxy_read_timeout 30s至关重要——它设定了Nginx等待后端响应的最长时间若后端处理超时如大促时库存扣减慢Nginx会主动断开连接并返回504避免用户无限等待。数据库采用MySQL 5.7主从复制。主库Master配置my.cnf[mysqld] server-id1 log-binmysql-bin binlog-formatROW expire_logs_days7 max_binlog_size100M从库Slave配置[mysqld] server-id2 relay-logmysql-relay-bin read_only1主库执行GRANT REPLICATION SLAVE ON *.* TO repl% IDENTIFIED BY repl123;授权从库执行CHANGE MASTER TO MASTER_HOST192.168.1.10, MASTER_USERrepl, MASTER_PASSWORDrepl123, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS154; START SLAVE;。应用层通过AbstractRoutingDataSource动态路由读操作走从库slaveDataSource写操作走主库masterDataSourceTransactional方法内强制走主库。这样读写分离将数据库压力分散主库专注写从库分担80%的查询流量。Redis部署哨兵模式Sentinel保障高可用。配置3个Sentinel节点监控主从实例。redis.conf中设置port 6379 bind 0.0.0.0 protected-mode no requirepass yourpassword slaveof 192.168.1.10 6379sentinel.conf中sentinel monitor mymaster 192.168.1.10 6379 2 sentinel down-after-milliseconds mymaster 30000 sentinel failover-timeout mymaster 180000 sentinel auth-pass mymaster yourpasswordSpring Boot中RedisConfig配置RedisSentinelConfiguration指定master名称和Sentinel节点列表。当主节点宕机Sentinel自动选举新主应用无感知切换JedisPool自动重建连接。4.3 性能压测与调优从JVM参数到SQL慢查询的实战优化上线前必须压测。用JMeter模拟1000用户并发下单初始配置-Xms2g -Xmx2g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize256m结果TPS仅120GC频繁。分析jstat -gc pid发现YGC每秒3次FGC每分钟1次Eden区几乎满。调优第一步JVM参数精细化。改为-Xms4g -Xmx4g -XX:MetaspaceSize512m -XX:MaxMetaspaceSize512m -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:G1HeapRegionSize2M。G1垃圾收集器更适合大内存MaxGCPauseMillis200设定停顿目标G1HeapRegionSize2M避免大对象直接进入老年代。调整后YGC降至每10秒1次FGC消失TPS提升至280。第二步数据库慢查询治理。开启MySQL慢查询日志slow_query_logONlong_query_time1压测后分析mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log发现SELECT * FROM order_item WHERE order_id ?未走索引。在order_item表上添加复合索引ALTER TABLE order_item ADD INDEX idx_order_id (order_id);该查询耗时从120ms降至3ms。第三步Redis缓存策略升级。原Cacheable(key#id)缓存商品详情但未设置过期时间导致热点商品缓存永久驻留。改为Cacheable(key#id, cacheNamesproduct, unless#result null)并在CacheConfig中配置Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { ... }设置RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(2))缓存2小时自动过期避免数据陈旧。最终经过三轮调优同一硬件环境下TPS稳定在420平均响应时间300ms错误率0.1%满足日单量5万的业务目标。5. 常见问题排查与避坑指南那些只有踩过才懂的实战经验5.1 启动失败ClassNotFoundException与NoSuchMethodError的根因定位Spring Boot项目启动报ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration表面看是类找不到实则是依赖版本冲突。pom.xml中若同时引入spring-boot-starter-web和spring-webmvc的旧版本如5.2.x而Spring Boot 2.7.x要求spring-webmvc5.3.x就会因包路径变更导致类加载失败。排查方法mvn dependency:tree -Dverbose | grep spring-webmvc查看实际引入的版本。解决方案删除显式声明的spring-webmvc依赖让spring-boot-starter-web的BOMBill of Materials自动管理版本。另一个高频问题是NoSuchMethodError: org.springframework.util.Assert.state(ZLjava/util/function/Supplier;)V。这通常发生在JDK版本不匹配时。Spring Boot 2.7.x编译目标为JDK 8若用JDK 11编译的jar包在JDK 8环境运行某些新增的Assert.state重载方法不存在。验证方式java -version确认运行时JDKjavap -cp your-app.jar org.springframework.util.Assert查看字节码中的方法签名。务必保证编译JDK与运行JDK一致或在pom.xml中指定maven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.target。5.2 接口404DispatcherServlet未注册与Controller路径错配访问/api/user/login返回404可能原因有三一是RestController注解缺失导致Spring MVC未扫描到该类二是RequestMapping(/api)写在类上但方法上又写了PostMapping(/api/user/login)造成路径重复为/api/api/user/login三是SpringBootServletInitializer未正确继承导致WAR包部署到外部Tomcat时DispatcherServlet未注册。最隐蔽的坑是组件扫描路径错误。SpringBootApplication默认扫描主类所在包及其子包若UserController在com.example.shop.controller而主类在com.example.Application少了一级shop则Controller不会被加载。解决方案在主类上添加ComponentScan(basePackages com.example.shop)或调整主类位置到com.example.shop.Application。5.3 数据不一致事务失效与Redis缓存穿透的连锁反应用户反馈“下单后库存没扣减”日志显示OrderService.createOrder()执行成功但product_sku表stock字段未变。根源往往是事务传播行为误用。createOrder()调用了skuStockService.reserveStock()而后者方法上也有Transactional默认Propagation.REQUIRED导致两个方法在同一事务中。若reserveStock()抛异常整个事务回滚但若它只是返回false库存不足而createOrder()未捕获此返回值继续执行就会出现“订单创建成功但库存未扣”的假象。修复方案reserveStock()改为抛出RuntimeException或createOrder()中显式判断返回值并throw new StockNotEnoughException()。缓存穿透问题更棘手。恶意请求/api/product/detail?id-1Redis无此key查DB返回null未缓存null值导致后续相同请求持续打DB。解决方案ProductService.detail()中若productMapper.selectById(id)返回null执行redisTemplate.opsForValue().set(product: id, NULL, 2, TimeUnit.MINUTES)并约定前端收到NULL字符串即视为商品不存在。同时用布隆过滤器Bloom Filter在Redis前拦截非法IDguava库的BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01)可过滤99%的无效请求。5.4 日志混乱Logback配置不当与敏感信息泄露线上日志中频繁出现java.lang.NullPointerException但堆栈指向UserController.login()的第45行而该行只是String phone loginDTO.getPhone();——明显不可能空指针。真相是Logback异步日志丢失上下文。appender nameASYNC classch.qos.logback.classic.AsyncAppender配置下MDCMapped Diagnostic Context中的traceId等信息无法传递到异步线程。解决方案在AsyncAppender中添加includeCallerDatatrue/includeCallerData或改用logback-access结合Nginx日志或干脆禁用异步用RollingFileAppender配合encoder的%X{traceId}占位符。另一个严重问题是日志打印密码明文。LoginController.login()方法中若log.info(login request: {}, loginDTO)而loginDTO包含password字段日志文件里就会出现password:123456。规避方法LoginDTO类重写toString()对敏感字段返回***或用Data注解时ToString.Exclude标注password字段最彻底的是在logback-spring.xml中配置filter classch.qos.logback.core.filter.EvaluatorFilter用JaninoEventEvaluator过滤含password的日志。提示所有线上环境必须关闭devtools和h2-consoleapplication-prod.yml中设置spring.devtools.restart.enabledfalsemanagement.endpoints.web.exposure.includehealth,info,metrics严禁暴露/actuator/env或/actuator/beans。注意MySQL主从延迟超过30秒时select for update可能在从库上执行导致幻读。务必确保读操作走主库或在Transactional方法内执行读写。6. 后续演进与扩展建议从单体到可扩展架构的平滑路径这个Spring Boot后端系统不是终点而是本文还有配套的精品资源点击获取
返回列表