ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue企业级免税商城系统设计与实现全解析

SpringBoot+Vue企业级免税商城系统设计与实现全解析 做免税商品商城系统和做普通电商系统给人的感觉完全是两码事。去年我接到一个企业级免税购物商城的改造需求原本以为只是一次常规的SpringBootVue商城开发结果在需求梳理阶段就被泼了冷水——免税品的类目管理、旅客身份校验、额度核算、完税价和免税价的双轨展示每一件事都比普通商城多出一层逻辑。整个系统最终用的是SpringBootVueMyBatisMySQL这套经典组合实现的但真正让系统在企业级场景下站稳脚跟的是业务建模阶段对免税规则的充分尊重以及后续在订单状态机、库存并发、权限安全这些环节上的细致处理。这篇博文围绕这套企业级免税商品优选购物商城管理系统的源码实现展开适合正在做商城类项目、准备用SpringBootVue技术栈入门企业级开发的读者。我会按业务差异、技术选型、模块拆解、数据库设计、安全与部署的顺序把关键实现思路和踩过的坑一次性讲透争取让你看完之后既能理解这类系统的设计逻辑也能直接拿去在自己的项目中参考落地。1. 免税商城和普通电商的差异业务建模必须先想清楚的三件事1.1 商品维度免税品不是普通货架的搬运工我拿到需求第一件事不是建表而是把免税业务的专业名词理清楚。普通商城只需要商品、SKU、库存、价格但免税商城多了一个核心约束——销售渠道。口岸出境免税、离岛免税、市内免税店每种渠道对应的可销售对象、限额标准、商品目录都不一样。一个商品可能在这个渠道允许销售在那个渠道就不允许甚至同一个SKU在不同渠道的定价策略、折扣规则、可购买数量都不同。所以设计上不能照搬普通商城的商品表结构。我当时落地时采用了商品基础表加渠道映射表的组合方式商品表只维护品牌、名称、规格、完税价、原产地这些基础信息渠道映射表单独记录每个SKU在某个免税渠道下的销售价、剩余额度、限购数量、上下架状态。这样运营人员在后台配置渠道策略时不需要改动代码和基础商品数据直接维护映射关系就行。这种解耦设计看起来多了一张表但在后期应对业务规则调整时省了非常多的事。另外一个容易忽略的点是税费展示。免税商城的核心卖点之一就是价格优势用户界面需要同时展示市场参考价完税价和免税价甚至要标明节省金额。这个逻辑涉及到税费的计算展示建议不要在SQL里做计算而是在后端统一封装一个价格服务根据商品的税种参数算出各个价格维度前端只负责渲染。我在实际项目中就是因为一开始把价格计算散落在前端导致后期调价时要改多个页面重构后才稳定下来。1.2 用户维度不是注册了就能买免税商城对用户身份的校验比普通电商严格得多。在普通商城用户注册登录理论上就可以下单但免税场景下用户在购买前必须经过身份资格的校验——是不是符合免税购买条件的旅客、有没有超过年度免税额度、本次购买的金额有没有超限。这些校验如果漏掉系统就不是一个合规的免税商城了。我在系统中专门设计了一套身份校验与额度管理流程用户在首次购买前需要提交证件信息以及行程信息后台调用核验接口确认资格后系统给用户打上“已验证”的标识同时记录其免税额度的已用金额和剩余金额。每次下单时订单服务会先读取额度快照校验本次订单金额是否在剩余额度内校验通过后预扣额度最后在支付成功时才真正占用额度如果订单取消或超时未支付额度自动释放回滚。这个额度预扣逻辑是容易被忽视的细节。最开始我图省事只在支付成功回调里扣减额度结果出现了一个问题用户同时下单两笔每笔都在额度内但两笔合起来超了总限额。后来改成下单时预扣、支付失败或取消时释放才彻底解决了超额购买的漏洞。做免税系统额度并发控制是无论如何都不能省的一步。1.3 价格优惠维度免税价与会员权益叠加的复杂度免税商城并不是简单的“标一个免税价然后下单”它同样有会员积分、优惠券、限时活动、满减促销等玩法的叠加需求。难点在于免税商品的价格基础本身已经经过一轮特殊计算完税价、免税价、市场参考价的对比上层再叠加优惠策略时必须有一套统一的规则引擎来规定叠加顺序。我当时采用的是“价格计算链”的方式基础价格 - 会员等级折扣 - 优惠券抵扣 - 满减活动 - 积分抵扣每一步都通过独立的策略类实现按配置的顺序依次调用。这样运营可以在后台配置每种活动的优先级而不是把逻辑写死在代码里。这里面最需要注意的是优惠后的最终实付金额不能低于系统允许的最低金额尤其要防止“优惠券叠加到零元甚至负数”的情况同时每个订单的实付金额和占用的免税额度要保持联动逻辑清晰避免后续对账时两边对不上。2. 技术选型复盘SpringBootVueMyBatisMySQL这套组合的取舍2.1 SpringBoot作为后端骨架省掉的是配置换来的是规范很多团队现在做企业级项目依然选择SpringBoot不是因为它最先进而是因为它能够把工程结构规范化和快速交付这两件事平衡得最好。在这个免税商城项目里SpringBoot负责的是整体后端架构通过Maven多模块方式拆分了common、system、product、order、member等多个模块模块之间依赖清晰编译和测试的边界明确。SpringBoot的自动配置特性在起步阶段尤其省心。比如引入spring-boot-starter-web就搭好了内嵌Tomcat的Web环境引入spring-boot-starter-validation就能用注解方式统一做参数校验不需要像传统SSH那样去写一堆XML配置。事务管理方面直接用Transactional注解声明在Service层的方法上配合Spring的声明式事务就能保证订单创建、库存扣减、额度占用这些跨表操作要么全部成功要么全部回滚。企业级项目不能只看开发效率还要看可维护性。SpringBoot默认的分层架构Controller-Service-Mapper经过多年验证对团队协作很友好前端同学对接接口文档后端同学按模块开发新入职的人也能很快定位到某段业务逻辑。这个项目里我用Spring Security做认证授权配合JWT实现无状态会话SpringBoot的starter机制让集成变得非常简单。2.2 MyBatis在复杂查询和动态SQL上的优势恰好命中商城业务商城这类业务十有八九离不开多条件筛选。商品列表页的筛选条件可能包含品牌、类目、价格区间、渠道、上下架状态、优选标签等而这些条件用户不一定每次都会选后台管理系统的订单查询更是如此。MyBatis的动态SQL在这种场景下简直是为它量身定做的。我用一个典型的商品列表查询做例子如果用户选择了“品牌是A、价格区间100到500、仅看有货”那么SQL就要动态拼接这些where条件。用MyBatis的 加 标签可以简洁地实现多条件组合不会出现字符串拼接导致SQL注入的风险也不需要在代码里写一堆if else去拼查询语句。这一点在实际开发中的体验差异是很明显的。除了动态SQLMyBatis的一对多映射也帮我解决了一个很头疼的问题订单主表和订单明细表的查询。一次订单查询需要同时查出订单基本信息以及该订单下的全部商品明细传统JDBC写法要么写多次查询然后在代码里组装要么用复杂的外连接返回冗余行再手动去重。MyBatis的resultMap配置了collection标签后一次查询就能自动映射出嵌套的订单明细列表代码量少执行效率也可控。2.3 Vue在前端交互复杂度上的表现刚好接得住商城页面的花样免税商城的用户端页面交互并不轻松商品瀑布流、多条件筛选联动、购物车数量增减、订单状态跟踪、会员中心积分展示这些操作都需要前端有足够灵活的组件化和状态管理能力。Vue在这方面是相当成熟的选项。我用Vue 2 Element UI搭建了管理后台Vue 3 Vite Pinia搭建了用户端商城页面。管理后台需要的是成熟组件库和快速迭代Element UI的表单、表格、弹窗组件能直接覆盖大部分场景用户端需要更流畅的交互和更好的性能Vue 3组合式API配合Vite开发时的热更新体验要明显好于旧版本。提到视频功能商城某次运营活动需要播放m3u8格式的商品介绍视频Vue生态里直接集成了video.js插件配置一下播放源就能搞定不用前端从零实现流媒体播放逻辑。Vue Router的路由守卫在商城系统里也扮演了重要角色。用户未登录访问购物车或结算页时路由守卫会拦截跳转到登录页登录成功后带redirect参数跳回原页面。这一步体验做好之后用户可以顺畅地从浏览商品到结算下单不会因为登录环节中断了购物流程。2.4 MySQL作为核心存储稳定性和事务能力是电商业务的底线电商项目可以引入Redis做缓存、引入Elasticsearch做搜索但核心的交易数据最终还是要有一个可靠的持久化存储MySQL在这个位置上依然是最稳妥的选择之一。这套系统里所有不允许丢失的数据——用户信息、商品SKU、订单、支付流水、额度记录我都放在了MySQL里。MySQL的InnoDB引擎提供行级锁和事务支持这对订单创建和库存扣减来说是底线能力。一个订单的创建要同时操作订单表、订单明细表、库存表、额度表任何一个环节失败都会导致数据不一致必须依赖数据库事务保证原子性。如果用NoSQL数据库做主存储这类强一致性场景往往要花很大力气通过分布式事务去弥补复杂度会高一个量级。在这个项目里MySQL还承担了一部分报表统计工作。后台管理需要按天、按渠道、按商品维度统计销售额和订单量我通过创建合理的索引和定时汇总表来加速这种查询而不是每次都去扫全表。对于数百万级的订单表只要索引设计到位MySQL的查询性能在企业级场景下完全够用。这套系统没有盲目引入太多中间件技术栈保持精简反而让维护成本低了很多。3. 核心流程实现商品管理、优选排序、购物车与订单状态机3.1 SPU/SKU设计与商品上下架的管理细节商品建模是商城系统的地基。常用的做法是SPUStandard Product Unit标准产品单元加SKUStock Keeping Unit库存保有单位双层结构。SPU对应的是“商品”这个抽象概念比如一瓶某个品牌的精华水SKU则对应具体的销售单元比如“该精华水50ml装”和“该精华水100ml装”它们的价格、库存、条码都独立管理。在这套系统里SPU表保存商品的主图、详情、类目、品牌等公共信息SKU表保存规格名、规格值、价格、库存、SKU编码等差异化信息。后台管理商品时运营先建SPU再在SPU下维护多个SKU前端商品详情页根据用户选择的规格动态切换价格和库存。前端SKU联动选择是商城体验的细节需要根据规格组合动态计算哪些组合有效、哪些组合缺货Vue的computed属性在这个场景下能写出非常简洁的状态判断逻辑。上下架管理的细节经常被新手忽略一个SPU下的某个SKU缺货了应该只下架该SKU而不是整个SPU如果整个SPU都没有可售SKU才应该下架整个商品。我在设计接口时专门实现了这个规则下架操作会检查该SPU下所有SKU的状态运营操作时系统给出明确提示避免用户端出现“点击商品后所有SKU全部无货”的尴尬页面。这个细节很小但对用户体验的影响很直接。3.2 优选推荐加权排序比协同过滤更务实标题里提到的“优选商品”并不是噱头它需要一个具体可落地的排序逻辑。很多团队一提到推荐就想着上协同过滤、上深度学习模型但对一个企业级的商城后台来说最务实的做法往往是用可解释的加权评分公式把运营想突出的商品推上去。我当时实现了一套优选商品排序服务给每个商品计算一个优选分计算公式综合考虑了销售额、销量、好评率、库存周转率、运营手工置顶权重这几个因子。比如优选分 销量得分 * 0.3 销售额得分 * 0.25 好评率得分 * 0.2 库存周转得分 * 0.15 运营权重 * 0.1。各项得分在计算前先做归一化处理避免销售额这种数值大的指标过度主导结果。这套打分逻辑通过SpringBoot的定时任务每天凌晨计算一次把商品ID和优选分写入一张优选排序表。用户端首页的“优选推荐”列表直接按这张表的分数倒序读取性能很好运营后台还支持单独调整某个商品的运营权重干预排名。相比黑盒的算法推荐这种加权方式逻辑透明、可调试、可解释上线后运营反馈非常好。等到业务数据量真正大了再在这个基础上引入更复杂的推荐算法也不迟底层的商品数据和行为日志已经提前埋好了点。3.3 购物车合并与选中结算的状态处理购物车模块看起来简单实际最容易出问题的是用户多端登录、商品下架、价格变动这几个场景。用户可能在手机上加购了几件商品又跑到电脑网页上登录购物车必须合并用户加购之后运营调整了价格结算时到底按加购时的价格算还是按最新价格算必须有明确的规则。我把购物车设计成独立模块数据存在MySQL的一张购物车表里字段包含用户ID、SKU号、数量、加购时间、选中状态。用户登录时如果发现本地有勾选的商品记录会调用合并接口写入数据库避免重复。在结算页展示购物车商品时后台会逐条校验商品状态已下架或已删除的商品自动标记为不可结算并提示用户价格以实时查询为准但页面同时展示加购时的价格如果价格有变动会明确提示让用户确认后再结算避免“结算后实际扣款和页面显示不一致”的客诉。结算接口接收用户选中的SKU以及数量列表后端在事务里批量锁定库存并生成订单。这里有一个必须处理的边界如果用户同时在两个终端对同一件商品发起结算后一个请求需要感知库存已经被锁不能继续下单。这个场景我在后面数据库设计章节会详细展开核心思路就是数据库行锁加唯一约束兜底。3.4 订单状态机与超时未支付自动取消订单模块是整个系统最核心的部分我花了不少时间设计订单状态机。订单状态不能只用一个字段随便改必须由明确的状态流转规则控制否则很容易出现“已取消的订单还能发货”这种严重事故。订单状态我设计了六个核心状态待支付、已支付、待发货、已发货、已完成、已取消。状态流转的触发点很明确用户发起支付且支付回调成功后待支付变已支付后台发货后已支付变已发货用户确认收货或系统自动确认收货后已发货变已完成用户主动取消或者系统超时取消则进入已取消状态。每种流转都封装在订单领域服务里只有该服务能修改订单状态Controller和其他Service不能直接更新状态字段从代码层面保证了状态流的严格性。超时未支付订单的取消是这套系统不能不处理的场景。用户下单后如果一直不支付库存就会被长期占用影响其他用户购买。我采用Spring的Scheduled写了个定时任务每分钟扫描一次待支付订单超过30分钟未支付的订单自动取消同时回滚库存和额度预扣记录。针对线上多实例部署的情况我在定时任务里加了一个分布式锁注解保证同一时间只有一个实例在跑这个任务防止重复取消同一个订单和重复释放库存。4. 数据库设计实战核心表结构、索引优化与防超卖方案4.1 核心表结构总览数据库设计是整个商城系统的地基表结构设计得是否合理直接决定了后期业务扩展的灵活性。这套商城的核心表有这些用户表、角色表、权限表、用户角色关联表、角色权限关联表、商品SPU表、商品SKU表、渠道映射表、购物车表、订单表、订单明细表、支付流水表、优惠券表、用户额度表、优选排序表、操作日志表。订单表和订单明细表是典型的主从结构订单表一条记录对应订单整体信息包括订单号、用户ID、订单状态、应付金额、实付金额、优惠金额、收货信息、下单时间、支付时间等订单明细表则记录订单里每一个商品的快照信息包括SKU号、商品名称、规格、单价、数量、小计金额。这里强调一点订单明细里的商品名称、单价必须冗余保存商品当时的快照不能通过SKU号去关联实时商品表否则商品改名或者调价之后历史订单的明细就全变了。用户额度表在这个项目里有特殊价值。字段包括用户ID、年度剩余额度、已用额度、最近占用时间每次下单预扣额度时更新剩余额度取消订单时回补。这个表的数据变化频率高要注意给user_id加上唯一索引并且配合悲观锁或乐观锁控制并发更新我在额度回补时采用的是CASCompare And Swap更新方式更新条件里带上剩余额度旧值更新后将受影响行数如果为0说明并发冲突重试一次。4.2 防超卖数据库行锁与乐观扣减超卖是电商系统开发绕不开的问题免税商城同样要面对。所谓超卖就是高并发场景下多个用户同时购买同一件商品导致卖出的数量超过了实际库存。这个问题如果在代码层用先查询再判断再更新的话并发请求同时读到剩余库存是100都认为可以购买结果一起扣减到80实际库存只剩下95超卖了5件。正确做法是使用数据库原子的条件更新。我在扣减库存时直接执行这样的SQLUPDATE product_sku SET stock stock - #{count} WHERE sku_id #{skuId} AND stock #{count}这个SQL利用InnoDB的行锁机制同一时刻只有一个事务能更新这一行的库存。执行完成后检查受影响行数如果等于1说明库存充足且扣减成功等于0说明库存不足或并发冲突直接返回“库存不足”给前端。这一步在任何语言里实现都一样的逻辑关键点是不要先查再改而是把库存判断和扣减合并成一条原子SQL。库存扣减之后紧接着就是创建订单。为了彻底防止同一用户在同一瞬间提交重复订单我在订单表里对用户ID和订单号的关键业务字段做了唯一约束兜底即使接口被重复调用第二次插入也会因为唯一索引而失败事务回滚后库存也会自动恢复。靠数据库兜底比单纯依赖业务代码判断要可靠得多。4.3 分页与查询性能优化商城后台的订单列表、操作日志、商品列表都是分页查询的重灾区。数据量小的时候感觉不到问题一旦订单表数据量到几十万甚至上百万普通的limit深分页会越翻越慢。原因很简单limit 100000, 20这样的SQLMySQL会把前100000行全部扫描出来再丢弃只返回最后20行这个开销是巨大的。我当时优化的手段是延迟关联。先让SQL只查询主键ID完成分页再用主键ID去关联查询完整记录这样能大幅减少回表的行数。改造后的SQL大概长这样SELECT o.* FROM t_order o INNER JOIN ( SELECT id FROM t_order WHERE order_status 2 ORDER BY create_time DESC LIMIT 100000, 20 ) t ON o.id t.id这个优化的效果很明显配合排序字段创建联合索引后深分页的响应时间从原来的秒级降到了百毫秒级。另外后台的复杂筛选查询我根据常用的查询条件建立了几个联合索引严格遵循最左前缀原则比如用户ID加下单时间、订单状态加下单时间确保查询能命中索引而不是全表扫描。5. 权限安全与支付回调企业级系统最容易翻车的三个环节5.1 RBAC权限模型与JWT会话管理企业级商城系统不像个人项目后台管理通常有超级管理员、运营、客服、财务、仓库等多个角色不同角色能访问的菜单和接口完全不同。我采用经典的RBACRole-Based Access Control基于角色的访问控制模型把用户、角色、权限三级解耦。用户不直接绑定权限而是通过角色间接获得权限新增一个运营岗位只需要创建角色并配置权限然后把用户加入该角色即可维护起来非常灵活。认证授权这块我选择了Spring Security加JWT的组合。用户登录成功之后后端生成一个包含用户ID和角色信息的JWT令牌返回给前端前端把令牌存储在本地每次请求在HTTP头里带上Authorization字段。后端通过Spring Security的过滤器链解析令牌、校验签名和有效期并把当前用户信息放入SecurityContext供后续业务逻辑获取当前操作人。JWT方案相比传统的Session方案最大的优势是后端服务可以无状态化多个应用实例之间不需要共享Session存储利于水平扩展。但JWT也有一个需要重点处理的隐患令牌在有效期内无法主动失效。我针对这个痛点做了两层弥补一是JWT的有效期控制在两小时以内降低令牌泄露后的风险窗口二是关键操作如修改密码、注销登录后会将该用户的令牌版本号加一解析令牌时发现版本号不匹配就要求重新登录。5.2 支付回调验签与幂等处理商城系统接支付是不可避免的这里最容易翻车的是支付回调的安全校验和重复通知处理。支付平台下单成功后会异步通知后端接口这个通知接口直接面向公网如果不做签名验证任何人都可以伪造一个“支付成功”的回调把订单状态改成已支付而不真正付钱这是极大的安全漏洞。我的实现逻辑是在发起支付时生成一个订单号和签名支付回调接口收到通知后先验证回调参数里的签名是否合法用支付平台配置的回调密钥重新拼接参数、计算签名做比对签名验证通过后再检查回调里的订单号是否存在、订单金额是否和数据库中的应付金额完全一致最后才把订单状态更新为已支付。金额必须精确到分进行比对防止篡改金额攻击。支付平台为了确保通知送达通常会重复推送回调频率可能是15秒、30秒、1分钟、5分钟这样阶梯递增。所以回调接口还必须是幂等的同一笔订单重复收到回调不能重复处理。我用订单状态做判断只有当订单处于待支付状态时才更新为已支付已支付状态直接返回成功响应拒绝重复处理。为了避免并发回调同时进入业务逻辑在更新订单状态的SQL上加上“WHERE order_status 待支付”的条件利用数据库层面的原子性来保证幂等。5.3 敏感数据脱敏与审计日志企业级系统离不开合规要求用户手机号、证件号、收货地址这些都属于敏感信息。在数据库里用户手机号和证件号不能明文裸存至少要做到接口返回时脱敏。我封装了一个脱敏工具类后端返回给前端的数据中手机号自动显示为138****1234证件号只保留前后各一位这样客服在后台即使有权限查看用户信息也只能看到必要的信息降低敏感数据泄露的风险。另一个容易被忽略的环节是审计日志。后台的关键操作——商品上下架、价格修改、订单强制取消、用户额度调整每一步都必须记录操作人、操作时间、操作内容和变更前后的数据。我在苍穹系统里用一个自定义注解加上AOP切面统一记录这笔日志不需要在每个业务方法里手写日志代码。审计日志平时看着不起眼但真出了问题排查是谁改错了数据、什么时候改的没有它是寸步难行的。这也是企业级系统和个人项目在认真程度上的核心差别。6. 部署上线与踩坑笔记从本地运行到稳定可用6.1 部署架构与流程整个系统部署时我用的是经典的前后端分离方案Vue前端代码通过npm run build打包成静态资源部署在Nginx的静态目录下SpringBoot后端打成可执行的Jar包通过systemd或者Docker容器运行在应用服务器上MySQL单独部署在一台机器上通过内网地址让后端应用连接。Nginx同时承担反向代理职责前端页面发起的/api/开头的请求被Nginx反向代理到后端的端口这样前端和后端就通过同一个域名提供服务不用处理跨域问题。部署流程我尽量自动化。用Git管理代码在服务器上拉取代码后用Maven打包前端用Node环境构建整个过程写成一个部署脚本。虽然这个项目没有上整套CI/CD流水线但脚本化部署之后升级版本也就是一条命令的事情。生产环境的MySQL我开启了定时备份每天凌晨全量备份一次数据库binlog日志保留七天这样即使误删了数据也能恢复到最近的时间点。企业级系统要稳备份策略绝对不能省。6.2 前后端分离部署的经典坑前端打包部署后最经典的问题就是刷新页面404。原因是Vue在history路由模式下浏览器访问某个子路由比如/member/order会直接请求Nginx服务器上该路径对应的静态文件而Nginx静态目录下并没有这个文件于是返回404。这个问题几乎每个用Vue history路由并前后端分离部署的项目都会遇到。解决方案是在Nginx配置里加入try_files指令让所有非静态资源的请求都重新指向index.html由Vue Router接管路由解析location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这个配置的含义是如果请求的文件不存在就回退返回index.html前端框架再根据当前URL的path渲染对应的页面。修改Nginx配置后需要reload生效。这个坑虽然好解但如果没有提前知道上线当天现场排查会非常狼狈当时我们团队第一次部署时就在这个上面卡了快一个小时。6.3 MyBatis批量操作与连接池的坑系统开发过程中还遇到过几个技术层面比较典型的坑第一个是MyBatis批量插入的性能问题。商城初始化商品数据时运营会一次性导入几百上千个SKU我最初用的是for循环逐条插入结果导入几百条数据花了十几秒体验极差。后来改成了MyBatis的foreach标签拼接批量插入SQL一次插入几百条耗时从十几秒降到了几百毫秒。insert idbatchInsertSku parameterTypelist INSERT INTO product_sku ( spu_id, sku_code, spec_name, spec_value, price, stock, create_time ) VALUES foreach collectionlist itemitem separator, (#{item.spuId}, #{item.skuCode}, #{item.specName}, #{item.specValue}, #{item.price}, #{item.stock}, NOW()) /foreach /insert使用批量插入要注意SQL语句的长度限制单条SQL太长会超过MySQL的max_allowed_packet限制我做了分批处理每批500条既保证了性能又不会超出限制。第二个坑是数据库连接池耗尽。系统上线初期跑了一段时间后运维反馈线上偶尔出现“连接池连接获取超时”的报错。排查后发现是某个查询接口在异常情况下没有正确关闭数据库连接在高并发时把连接池的连接占满了。后来一是确认所有查询都通过MyBatis自动管理连接二是给关键接口增加了慢查询日志监控把查询时间超过两秒的SQL抓出来逐一优化从源头减少连接占用时间。6.4 定时任务在多实例下的幂等保障系统上线稳定运行后团队开始做多实例部署提升可用性。这一做就暴露出一个问题原本写好的超时订单取消定时任务在多个应用实例同时启动时会重复执行。两个实例同时在扫描待支付订单就可能出现同一个订单被两个实例重复执行取消逻辑虽然业务上不会导致严重的资金问题但会导致库存回滚两次、额度释放两次的隐患必须处理。我当时选了一个轻量级的方案在数据库里建一张分布式锁表定时任务执行时先尝试插入一条指定任务名的记录插入成功表示拿到锁执行完业务后删除记录插入失败表示其他实例正在执行该任务当前实例直接跳过。这个方案实现简单基于数据库唯一索引天然保证只有一个实例能插入成功对当前这个体量的系统来说完全够用。如果未来实例数量进一步增加可以再考虑引入专门的分布式锁组件但现阶段过度设计反而是负担。这个经历给我的感受很深很多问题在开发环境永远发现不了只有真正部署上线、迎接真实流量之后才会暴露出来。所以一个企业级项目从开发到稳定可用中间那段距离恰恰是踩坑、填坑、总结经验的过程。我把这些过程整理出来就是希望大家做同类系统时能少走这些弯路把精力更多放在真正有业务价值的设计上。
返回列表