news 2026/9/29 16:46:47

SpringBoot+Vue前后端分离商城实战:数据库设计、订单并发与部署避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue前后端分离商城实战:数据库设计、订单并发与部署避坑指南

做这个项目的时候,我最初的想法特别简单:需要一个能完整跑通的电商系统作为技术练习,于是就有了“米家商城”这个名字。项目严格来说是两块:一块是面向消费者的商城前端,一块是运营人员使用的后台管理端,也就是项目代号里的“abo管理系统”(Administrative Back Office,管理后台)。整套系统基于SpringBoot + Vue + Java + MySQL + MyBatis实现,前后端分离,后端提供RESTful接口,前端负责页面展示和交互。对于刚接触JavaWeb完整流程的同学来说,这个项目的价值在于它把SpringBoot配置简化、MyBatis持久层操作、Vue组件化开发,以及数据库建模这些知识点串成了一条线。

1. 项目整体架构与技术选型

1.1 为什么是SpringBoot + Vue

网上关于Java后端框架的选择,早几年还在争论SSM和SpringBoot,最近几年基本没什么争议了。SpringBoot能火起来,核心原因是它把Spring全家桶的XML配置压缩成了自动配置,一个main方法就能启动Web服务。做商城这种CRUD密集型项目,SpringBoot的起步依赖和自动配置能省掉大量重复工作,比如内嵌Tomcat、DataSource自动配置、事务管理器自动装配,开箱即用的体验对新手特别友好。Vue这边,项目用的是Vue2,不是因为它比Vue3更优秀,而是当时的组件生态和团队成员更熟悉,Vue的组件化对商城这种模块多、复用性强的场景非常合适,商品卡片、分页器、订单状态标签、导航栏这类UI都可以抽成组件,配合props和事件就能在不同页面复用。SpringBoot和Vue组合后,团队可以并行开发:后端先定义好接口文档和数据库表结构,前端用mock数据同步开搞,最后联调接口。这种开发模式在中小型项目里效率提升非常明显。

这套架构还有一个好处是部署灵活。后端最终打成可执行jar包,前端打包成静态文件扔到nginx里,不需要一台服务器同时跑Tomcat和前端静态服务。接口和页面分离后,后期如果要扩展小程序端或App端,只需要把前端页面换成uni-app或者React Native,后端接口完全复用。所以这个选型不只是因为主流,更是因为它能覆盖从单体应用到前后端分离的演进路径,对毕设或者个人项目来说,性价比很高。

1.2 米家商城和abo管理系统的功能边界

整个项目按用户角色拆成两个端。米家商城面向普通用户,功能包括注册登录、首页商品展示、分类浏览、关键字搜索、商品详情、购物车管理、订单结算、模拟支付、个人中心查看订单状态。abo管理后台面向管理员,功能包括管理员登录、控制台数据统计、商品分类管理、商品上下架、订单审核与发货、用户禁用与解封、系统参数配置。两个端共用同一个后端服务,通过接口路径前缀区分,前台接口统一以 /api/user/ 开头,后台接口以 /api/admin/ 开头,后端拦截器根据路径判断是否需要权限,避免用一个巨大的if-else去拆功能。

“abo”这个名字其实是项目立项初期的内部代号,取自Administrative Back Office的首字母,直观理解就是办公室后台。对外展示时可能不太容易猜出来,所以我在项目文档里专门加了个注释。功能设计上,后台管理并不需要跟前台页面一一对应,比如商品模块在前台是搜索和浏览,在后台是增删改查和上下架;订单模块在前台是提交和查看,在后台是发货和售后处理。做需求拆分的时候,我建议先把每个端口的角色和操作列成一张矩阵,再开始建表和写接口,能少走不少弯路。

1.3 技术栈细节与版本选择

数据库用的MySQL,版本是8.0.x。MySQL 8.0相比5.7在性能、窗口函数、JSON类型上都有提升,但使用中也有坑,后面会专门讲。驱动必须写com.mysql.cj.jdbc.Driver,并且在连接参数里加serverTimezone=Asia/Shanghai。持久层框架我选了MyBatis而不是MyBatis-Plus,因为想锻炼原生SQL能力,实际开发中MyBatis对复杂查询的控制力更强,比如多表关联、动态条件、批量插入,都能通过手写SQL解决。MyBatis-Plus的代码生成和条件构造器确实方便,但那个是从ORM角度思考问题,一旦SQL复杂反而要绕弯子。这个项目到了后期,我也引入了PageHelper做分页,只是为了节省写limit的时间。

前端工具链用了Vue CLI创建项目,Node版本固定在14.x,因为高版本Node在安装旧依赖时容易报OpenSSL错误。后端JDK用的1.8,SpringBoot版本选的2.3.x,这个组合兼容性非常稳。构建工具方面,后端用Maven管理依赖,前端用npm。整套环境搭建下来,最花时间的其实是前端依赖下载,建议使用国内npm镜像,能省去大量等待时间。项目初期我把前后端仓库分开管理,后来为了交代码方便,又合并到一个仓库,分别放在server和web两个目录下,各自维护独立的package.json或pom.xml。

2. 数据库设计与核心表结构

2.1 从“用户-商品-订单”三个主维度建模

做商城系统,数据库设计是第一步,也是最容易返工的一步。我的做法是先把业务对象列出来,再画ER图,最后才落到建表SQL。核心对象是用户、商品、分类、订单、订单明细、购物车、收货地址。用户表user字段包括id、username、password、nickname、avatar、phone、email、status、创建时间和更新时间。password字段存的是BCrypt加密后的散列值,长度要留到60以上。商品表product字段包括产品编码、标题、副标题、主图、轮播图、价格、原价、库存、销量、分类id、上下架状态、详情富文本。这里必须强调:价格和库存用decimal和int,不要用float,否则金额比较会出问题。分类表category用parent_id做自关联,支持无限级分类,实际商城用两级就够了。订单表orders保存订单编号、用户id、总金额、支付状态、收货地址快照、订单状态。订单明细order_item保存商品快照,包括商品标题、图片、价格、数量,防止商品改价或下架后历史订单数据错乱。

建表时我统一加了created_at和updated_at两个字段,用datetime类型,应用层写入。物理外键我没有用,只在实际字段上建了普通索引。原因有两个:一是物理外键在插入、更新时会额外触发约束检查,高并发下影响性能;二是后期如果要分库分表,物理外键几乎无法迁移。所以表之间的关系靠Service层保证,冗余数据可以通过MySQL的JOIN查询来关联,逻辑清晰,扩展性也好。

2.2 购物车与库存扣减为什么单独建表

购物车单独建cart表,字段包括用户id、商品id、数量、选中状态。很多人会把购物车存Redis,Redis性能确实好,但考虑到这是教材级项目,要求“Java+MySQL”能完整跑通,所以用表持久化。这样用户换设备也能同步购物车,后期如果要做“降价提醒”或“购物车数量角标”,查表也很方便。购物车表设计了联合唯一索引(user_id, product_id),这样同一个商品重复加入时执行INSERT ON DUPLICATE KEY UPDATE,直接累加数量,不用先查再更新。表结构虽然简单,但这一步考虑了幂等,后续就不用担心插入重复记录。

库存字段直接放在product表的stock字段,下单时在事务里先执行SELECT stock FROM product WHERE id = ? AND stock >= ? FOR UPDATE,把商品行锁住,再执行UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?。这里特意用了带条件的更新,保证并发下库存扣减不会变成负数。行级锁会牺牲一点并发量,但对中小型商城完全够用,远比悲观锁一把锁锁全表靠谱。如果以后商品数量变大,可以再把库存拆到单独库表或Redis预扣方案,但一开始没必要上那么复杂的架构。

2.3 订单状态机与数据一致性

订单状态是商城系统的灵魂,我设计的订单状态码如下:0待支付、1待发货、2已发货、3已完成、4已取消、5已退款。为了排查问题,订单表还保存了订单备注字段,另外建了一张订单操作日志表,记录谁在什么时间把订单从什么状态改成了什么状态。状态流转不允许跳级,比如支付后必须从0切到1,发货后从1切到2,取消只能发生在0。这个规则在Service层控制,更新订单SQL强制带上当前状态,影响行数等于1才说明操作成功,否则就提示“订单状态已更新,请刷新页面”。这种简单方案比引入状态机引擎更容易理解,也满足项目需要。

订单和库存还有一个联动问题:下单时要预扣库存,实际支付后占用库存锁定,如果超时未支付则关闭订单并释放库存。我用Spring的@Scheduled注解写了一个定时任务,每30分钟扫描一次创建时间超过30分钟且状态为0的订单,先关闭订单再恢复库存。这里要注意定时任务要加分布式锁,否则多实例部署时会多个任务同时跑,造成库存恢复两次。我在项目里只部署了一个实例,所以用synchronized加签名就够了,但写文档时把这个风险写清楚了。

3. 后端关键功能设计与实现

3.1 RESTful接口设计与统一返回结构

后端接口设计坚持RESTful风格,资源用名词复数,操作交给HTTP方法。比如GET /api/user/products是查询商品列表,POST /api/user/orders是创建订单,PUT /api/admin/products/{id}是修改商品。所有接口返回统一结果对象Result,这个对象只有三个字段:code、message、data。code为200表示成功,401表示未登录,403表示无权限,500表示服务器异常。前端axios响应拦截器看到code不等于200时统一弹出提示,不需要每个页面写重复错误处理。接口入参校验用spring-boot-starter-validation,创建订单时收货地址不能为空、数量必须大于0,都通过DTO上的@NotNull和@Min注解解决,校验失败由全局异常处理器返回对应提示。

为了接口文档规范,我引入过Springfox生成Swagger文档,后来觉得页面太重,就改成在代码里写固定的接口注释。如果只是个人项目,用Postman的Collections也能管理接口,但Swagger能自动导入参数、导出在线文档,方便答辩和交接。要注意的是,Swagger在SpringBoot 2.6以上有路径匹配策略冲突,会启动报错,需要加spring.mvc.pathmatch.matching-strategy=ant_path_matcher。这个坑我遇到的时候排查了很久,写在这里提醒一下。

3.2 MyBatis动态SQL实战:多条件搜索与批量更新

MyBatis最强大的能力就是动态SQL。商品列表页有三个搜索条件:分类id、关键字、上下架状态。ProductMapper.xml里用where标签配合if标签拼条件,关键字用LIKE CONCAT('%', #{keyword}, '%'),注意不要直接写LIKE '%${keyword}%',那会被SQL注入。下面是我常用的写法:

<select id="selectProductList" resultMap="ProductResultMap"> SELECT p.*, c.name AS category_name FROM product p LEFT JOIN category c ON p.category_id = c.id <where> <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (p.title LIKE CONCAT('%', #{keyword}, '%') OR p.subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="status != null"> AND p.status = #{status} </if> </where> ORDER BY p.id DESC </select>

这段代码只列出两个条件,实际开发中别忘记拼接排序和limit。分页我用PageHelper,PageHelper.startPage(pageNum, pageSize)必须紧跟目标select语句,中间不能夹其他数据库操作,否则分页可能会作用到错误的SQL上。批量更新商品状态时,我用 标签遍历id集合,比如UPDATE product SET status = #{status} WHERE id IN (...)。这里要注意MySQL默认允许一次更新的包大小限制,如果id集合太大,建议分段批量执行。

3.3 登录鉴权:JWT + BCrypt

前后端分离后,Session方案不好扩展,我用了JWT。用户登录成功后,后端生成token,里面带上用户id和过期时间,用HMAC签名,前端把token存在localStorage,每次请求在Authorization头里带上。后端写一个拦截器解析token,从Header里拿到凭证,验签和过期校验通过后,把用户信息存进ThreadLocal,供Controller直接取。ThreadLocal用完要记得remove,否则线程池复用时会串数据。密码存储一定不要用MD5,MD5加盐能防彩虹表,但BCryptPasswordEncoder每次加密会产生随机盐,散列值抗暴力破解更强,是我现在的首选。登录接口还需要图形验证码,项目里接了个简单的验证码工具,生成一个base64图片和一个uuid,前端把uuid和用户输入的验证码一起提交,能防止接口被脚本刷。

JWT也有坑:token一旦签发没办法主动失效,除非引入黑名单机制。这个项目用户量不大,所以token有效期设置成7天,管理员token设置成30分钟,到期后需要重新登录。后续如果要做敏感操作比如改密码,可以强制前端更换token,或者在后端维护一个token版本号,这些都属于额外扩展,写毕设文档时可以提一句体现思考深度。

3.4 文件上传:图片资源的本地存储方案

商品图片上传是后台必有的功能。我用的方案很简单:前端用el-upload组件,action指向后端的/api/admin/upload接口,后端用MultipartFile接收,校验文件类型和大小,保存在服务器本地的/upload目录,文件名用UUID重命名防止冲突。保存成功后回传完整URL,比如http://服务器IP:8080/upload/xxx.jpg。然后为上传目录配置一个MvcResourceHandler,让SpringBoot把/upload/**映射到本地磁盘路径,等于实现了简易图床。为什么不建议把图片存数据库?二进制字段既浪费性能又难维护,存路径就够了,图片文件本身走静态资源服务。这里还有几个经验:上传接口要限制文件类型,我用白名单校验图片后缀;文件大小上限在application.yml里配置multipart.max-file-size,默认只有1MB,不调大一点图片很容易上传失败;本地目录要定期清理,否则磁盘会被无效图片占满。

4. 前端Vue项目搭建与页面实现

4.1 用Vue CLI初始化项目与目录规范

前端工程我用Vue CLI创建,命令是vue create mall-web,创建时选择Router和Vuex。Node版本需要注意,太高版本可能和旧版node-sass冲突,我固定Node 14。创建完成后的目录结构,src/api专门放接口调用,src/router放路由配置,src/store放Vuex状态,src/views放页面组件,src/components放公共组件。封装请求工具时,我在axios实例里设置了baseURL、超时时间以及请求拦截器,请求拦截器从store或localStorage取token,响应拦截器统一处理业务code,如果code为401就跳转登录页。API模块按后端接口对应拆分,比如product.js里放getProductList、getProductDetail等方法,页面只需要引入API方法,不直接调用axios,这样后端接口地址调整时只需改一个文件。

目录规范看起来是小事,但项目写到后面就知道,没有规范很容易在一堆组件里迷路。我的习惯是所有页面组件文件名和路由路径保持一致,公共组件都带上通用前缀,比如ProductCard.vue、PaginationBar.vue。后端接口的定义也按模块归到同一个文件里,这样调试接口时,只需要打开一个js文件,就能看到这个模块所有可用的请求方法。

4.2 Vue Router路由守卫与Vuex状态管理

商城有两种角色:普通用户和管理员。前台需要判断是否登录才能进入购物车和订单页面,后台需要判断是否管理员才能访问。路由配置里给后台路由加meta:{ requiresAuth: true, role: 'admin' },然后在beforeEach守卫里做判断,代码逻辑大致是:如果目标路由需要登录且没有token,跳转登录页;如果是后台路由且当前用户角色不是管理员,跳转403页面。Vuex主要存用户信息、token、购物车数量。购物车数量在用户登录后拉取一次,加入购物车时更新state,再调用后端接口持久化。后台菜单数据从数据库动态加载,也存到Vuex里,刷新页面后重新拉取。

这里有个非常经典的坑:页面刷新后Vuex会被清空。我在main.js里调用一个初始化方法,先从localStorage恢复用户基本信息,再请求一次用户详情接口,同时根据路由权限判断当前页面是否可访问。否则用户刷新后台页面时,路由守卫拿不到用户信息,会被误判为未登录,结果就是反复跳登录页,体验很差。做权限控制的时候,前后端都要做,前端控制按钮显隐,后端控制接口访问,两者缺一不可。

4.3 商城核心页面的交互逻辑

首页是商城门面。轮播图用第三方组件swiper,商品列表用flex布局的卡片组件,每个卡片点击跳转详情页。商品详情页图片可以放大,规格选择用radio分组实现,价格随规格变化。加入购物车按钮要处理两种情况:未登录先跳登录页,登录后调用cart接口。购物车页面支持修改数量、单选全选、删除,结算时把选中的商品传给订单确认页。订单确认页包含收货地址选择、配送方式选择、订单金额汇总,提交订单按钮要做防重复点击,点击后禁用并显示loading,防止用户连续提交产生多笔订单。这里我特意在订单号生成规则里拼接了毫秒级时间戳和随机数,保证订单号唯一。

订单支付是模拟的,我在支付页设计了一个“确认支付”按钮,点击后直接把订单状态从0改成1,同时清空购物车已下单商品。如果是真实支付,这里要对接微信或支付宝,流程会复杂很多,但对于毕设项目,模拟支付足够展示整个闭环了。个人中心页展示订单列表,按状态分类展示,用户可以取消订单、确认收货、申请退款。申请退款这里我留下了扩展点:后台退货审核页面可以按订单号搜索,通过后更新订单状态为5。

4.4 abo管理后台的表格与表单设计

后台管理系统CRUD居多。商品管理页面用的是el-table展示商品列表,操作列有编辑、上架、下架、删除。搜索区放在表格上方,点击查询时重新加载数据。编辑功能用el-dialog弹窗包裹表单,新增和编辑共用同一个弹窗,通过title和字段初始化来区分。表单校验规则用el-form的rules,比如商品标题必填、价格必须大于0、库存必须是整数。分类管理用element-ui的el-tree,支持增删改和拖拽排序,树节点的增删要注意同步刷新分类下拉框。订单管理页用状态筛选标签,点击发货时弹出物流单号输入框,提交后调用后台接口更新状态。后台的整体布局参考了vue-element-admin的风格,左侧菜单从路由自动生成,顶部加面包屑导航和用户下拉菜单。

表格和表单虽然看起来机械,但有几个细节值得注意:列表页的删除操作必须二次确认,不然误删很麻烦;表格加载状态用v-loading指令,避免用户以为是卡死;上传图片时el-upload的on-success回调要把返回的URL存到表单字段里,再把预览图赋值给图片组件的value。这些交互细节决定了管理系统好不好用,很多毕设项目功能都实现了,但体验很生硬,就是因为少了这些细节。

5. 前后端联调与部署上线

5.1 跨域问题的三种解决方式

前后端分离联调第一天大概率遇到跨域。我踩过的坑是:接口能从浏览器地址栏访问,但页面请求就报CORS。后来在vue.config.js里配了devServer.proxy,把/api代理到http://localhost:8080,开发环境不用后端配跨域。代理配置大概是这样:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

部署到服务器时用nginx做反向代理,location /api/ { proxy_pass http://127.0.0.1:8080; }。如果后端也要支持跨域,就写一个CorsFilter,配置allowedOriginPatterns和allowedMethods。三个方案优先级:nginx最稳定,开发代理最方便,后端CorsFilter最灵活。实际项目我建议前端代理加nginx配合使用,后端不一定要开跨域开关,偶尔开一下也要注意allowedOrigins别写成*,否则携带cookie时会有问题。

5.2 SpringBoot打包部署与Nginx配置

后端打包用mvn clean package,生成可执行jar。服务器上装JDK8,用nohup java -jar mall-server.jar --spring.profiles.active=prod后台运行。前端打包用npm run build,生成dist目录,把它放到nginx的html目录或指定root。nginx配置要点:location / 指向dist目录和index.html;location /api/ 代理到后端;history路由需要try_files $uri $uri/ /index.html,否则刷新页面就404。这个坑我印象深刻,第一次部署时把路由从hash改成了history,刷新二级页面直接白屏,加上nginx配置后才好。Nginx配置片段供参考:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

部署时还要注意端口放行和防火墙。如果后端用8080端口,前端用80端口,就得保证云服务器安全组规则里80和8080都能访问。我用的是CentOS 7.6,装nginx用yum install nginx就行,配置文件默认在/etc/nginx/nginx.conf,修改完记得nginx -t检查语法再reload。

5.3 MySQL数据迁移与初始化脚本

上线前准备一份init.sql,里面包含建库、建表和初始数据。因为本地MySQL版本是5.7,服务器是8.0,导入时要注意utf8mb4字符集设置,避免中文乱码。application-prod.yml里配置数据库连接,url中加useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。数据迁移我用的是mysqldump -u root -p mall > mall.sql,然后在服务器上source mall.sql。如果遇到外键冲突,检查表导出顺序或临时SET FOREIGN_KEY_CHECKS=0。上线后建议定期备份,至少mysqldump做全量备份,配合binlog做增量。数据备份这件事,我一开始完全没在意,直到有一次误删了一张表才追悔莫及,从那次后每次改动数据库前都先导出一份sql。

数据库初始化脚本最好包含测试数据,不然前端联调时列表空空,看不出效果。我准备了两个分类和十来个商品,图片用的是公共占位图。初始管理员账号是admin,密码是123456,但上线后必须改成强密码,我在文档里专门标注了这个风险点。

6. 常见问题与避坑指南

6.1 MyBatis字段映射与TypeHandler

数据库字段用下划线,Java属性用驼峰,如果没开启mapUnderscoreToCamelCase,查出来的字段全是null。我在application.yml里配置mybatis.configuration.map-underscore-to-camel-case=true。另一个容易踩的是枚举类型:Java里的订单状态如果定义成枚举,查询和插入都需要TypeHandler来处理。MyBatis的默认枚举处理器可以存name或者ordinal,但项目里要显示状态说明,所以我自定义了OrderStatusTypeHandler,在setNonNullParameter和getNullableResult两个方法里做转换。自定义TypeHandler的工作流程其实很简单:insert时把Java枚举转成数据库int,select时把int转回枚举,关键是注册到XML的resultType上。网上有大量关于TypeHandler的图解,但我个人觉得实际写一遍比看图更快,代码量很小,却能解决字段语义化的大问题。

6.2 MySQL 5.7与8.0的差异坑

连接驱动是最大差异。MySQL 5.7用com.mysql.jdbc.Driver,8.0用com.mysql.cj.jdbc.Driver,依赖版本换错启动就会报ClassNotFound。8.0还默认要求设置时区,我在url后面加了serverTimezone=Asia/Shanghai。密码认证方式:8.0默认caching_sha2_password,如果Java连接报“Unable to load authentication plugin”,需要创建用户时指定mysql_native_password。另外MySQL 8.0的GROUP BY更严格,查询字段必须在group by或聚合函数里,老SQL直接报错。这些坑在部署阶段特别常见,因为本地开发库和服务器生产库版本不一致,所以上线前一定要确认两边MySQL版本,避免生产环境跑不起来或者SQL执行结果不一样。

如果使用MySQL 8.0的JSON字段,MyBatis的resultMap映射也要适配,不能直接用普通字符串类型,需要自定义TypeHandler读取JSON。这个项目暂时没用到,但我加了一个备注字段做JSON格式的规格参数扩展点,以后可以无缝升级。

6.3 Vue项目的白屏与404问题排查

前端白屏时先看控制台有没有报错,如果只是空白,多半是路由模式问题。开发环境用history模式,刷新某个具体路由会404;生产环境nginx没配try_files也会404。还有一种白屏是代码编译错误,Vue运行时错误会在控制台提示,这时要看具体报错组件。如果build后的dist文件夹里index.html引用的JS路径是绝对路径,部署到子目录会找不到,需要把vue.config.js的publicPath改成'./'或相对路径。另外组件循环依赖也可能导致白屏,用Vue Devtools检查依赖关系会快很多。我调试的时候习惯在main.js入口先打印一个全局配置信息,确认前端代码是新的,避免缓存导致看到的还是旧版本。

6.4 并发下单与库存扣除的实践

刚开始做订单时没考虑并发,压测时发现100个并发请求能把库存扣成负数。后来我用select for update锁定商品行,再执行更新库存。但是要注意for update必须在事务里执行,不能锁定后抛异常,否则连接会一直被占用。为了更稳,我还在订单表创建了唯一索引order_no,防止重复生成订单。每隔半小时做一次未支付订单超时关闭,扫描创建时间超过30分钟且状态为0的订单,先关闭订单再释放库存。这个定时任务用Spring的@Scheduled注解实现,生产环境建议改成分布式任务,避免多实例重复执行。如果要用乐观锁,可以在product表加version字段,更新库存时set version = version + 1 where version = #{version},只要更新行数为0就重试,这种方案适合并发不是特别高的场景。

实际压测下来,100个并发用户同时买同一个商品,在select for update方案下,库存最终扣减数量正确,但接口响应时间会有轻微波动,因为排队等待行锁。这个结果完全可以接受。如果未来量大了,可以按商品维度做Redis预减库存,再异步同步数据库,那是另一个层次的问题了。

这个项目做完以后,我个人最大的体会是:一个商城系统看起来功能简单,真正动手做的时候,数据库设计和并发控制才是最花时间的。后端接口、前端页面这些都可以靠熟练度堆出来,但像订单状态、库存扣减这种业务规则,一旦设计错误,后面补丁只会越打越多。如果你正在做类似的SpringBoot+Vue商城项目,我建议先把订单和库存这两块想清楚,再写代码;数据库字段可以冗余一些,但状态流转的逻辑一定要理清。另外,部署上线时提前把MySQL版本和nginx配置确认好,能少熬几个夜。最后分享一个小技巧:所有配置信息,包括数据库密码、密钥、文件路径,都放到application-prod.yml里,本地用application-dev.yml,不要把生产数据库密码提交到代码仓库,这种教训我认为值得每个开发者早点记住。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 16:46:22

财务系统建设最难啃的硬骨头:业务规则、技术架构与避坑指南

做企业数字化做了这些年&#xff0c;有一个领域是公认的硬骨头——财务系统。市面上讲产品功能的文章很多&#xff0c;讲技术架构的也不少&#xff0c;但真正把难点掰开揉碎讲透的确实不多。财务系统难&#xff0c;不是难在哪个具体功能上&#xff0c;而是难在一套系统要同时满…

作者头像 李华
网站建设 2026/9/29 16:45:37

从零搭建AI工程体系:环境、数据、训练、部署与监控全链路实战

从零搭建AI工程能力这件事&#xff0c;我前前后后折腾过好几轮。最早的时候我也走过弯路——上来就装框架、跑Demo、调API&#xff0c;结果模型一换、数据一多、并发一上来&#xff0c;整个项目就散架了。后来我才慢慢想明白一个道理&#xff1a;AI工程不是"会调模型"…

作者头像 李华
网站建设 2026/9/29 16:44:20

starnet桌面AI Agent框架:MCP协议与OpenRouter模型路由实战

1. 从“starnet”这个名字说起&#xff1a;它到底想解决什么问题 第一次看到“starnet”这个项目标题&#xff0c;加上旁边一串热搜词——AI agents、desktop、OpenRouter、MCP——我脑子里第一反应是&#xff1a;这又是一个想把“AI 智能体”和“本地桌面环境”缝在一起的东西…

作者头像 李华
网站建设 2026/9/29 16:44:19

一文搞懂IPEX、SMA、U.FL射频连接器区别与选型

干我们这行&#xff0c;最常被小白问到的不是电路怎么画&#xff0c;而是天线接口怎么认。IPEX、SMA、U.FL这三个词&#xff0c;看着像三兄弟&#xff0c;实则是完全不同路子的连接器&#xff0c;但很多商家和教程又喜欢把IPEX和U.FL混着叫&#xff0c;导致你拿着卡尺量半天&am…

作者头像 李华
网站建设 2026/9/29 16:43:56

量子力学与材料力学:从密度泛函理论到弹性常数预测

1. 当材料力学开始问“为什么”&#xff1a;经典模型面对尺度极限时的空白材料力学这门学科&#xff0c;传统上是靠连续介质假设吃饭的。我们习惯把一块金属看成均匀的、连续的物质&#xff0c;用应力、应变、弹性模量去描述它在外力下的行为。这种思路在宏观尺度下极其成功——…

作者头像 李华
网站建设 2026/9/29 16:43:56

CH340G、CH340C、CH340N选型本质差异与工程避坑指南

1. 别被丝印骗了&#xff01;CH340系列不是“同款换壳”&#xff0c;而是三套不同设计逻辑的USB转串口方案你拆开手头那块Arduino Nano&#xff0c;或者刚焊好的ESP32开发板&#xff0c;翻到背面——大概率会看到一颗标着“CH340G”或“CH340C”的小黑片。它安静地蹲在USB接口旁…

作者头像 李华