Spring Boot电商项目实战:缓存、事务与分布式锁 简介一套基于SpringbootVue的网上电商项目采用前后端分离架构面向JavaWeb学习者与初中级开发者覆盖前台商品展示、购物车、订单查询与后台商品/订单/用户管理等典型业务场景。项目整合Springboot、Maven、Mybatis、Vue、Redis等技术并附带支付宝沙箱环境与支付环节代码可作为毕业设计或企业实训参考。压缩包共615个文件主要以Java源码、class文件、Vue组件、JS脚本、XML/yml配置、SQL脚本等形式呈现整体18.89MB目录结构清晰便于按功能模块查阅。目前已有297人学习下载。包内包含完整前后端工程、数据库初始化脚本、环境配置说明及常见问题处理信息下载后按说明配置Jdk1.8、Tomcat8.5与MySQL即可运行适合希望快速掌握电商项目全栈流程的读者。1. 网上电商项目到底在做什么一套前后端分离的Spring Boot全家桶网上电商项目前后端分离Java Vue Spring Boot SSM MySQL Redis这套组合说句不好听的几乎成了Java Web就业项目的标配模板。但真拿它去撑一个能上线的线上商城差距不在功能而在细节。我做过的电商项目少说三四个商品表、购物车、订单、支付回调业务逻辑都差不多真正拉开差距的是热点数据怎么缓存、库存怎么扣、并发下单时怎么保证不超卖。这篇文章就顺着这套技术栈把商品浏览、购物车、订单、缓存和事务这些核心链路拆开讲从工程骨架到参数配置再到我踩过的坑让你照着能复现而不是只会跑一个demo。2. 从零拉起电商项目骨架Maven多模块与Vue工程初始化2.1 单模块还是多模块电商项目该用哪种Maven结构很多教程把Spring Boot项目做成一个单模块工程controller、service、mapper全塞在一起几百行代码还能撑住一旦商品、订单、用户、支付、优惠券这些业务模块堆进来单模块会让你改一个支付回调都得重新编译整个工程。我一般会拆成多模块不是为好看是为了让mysql、redis、maven这些依赖的边界清晰也方便以后拆微服务。一个典型的电商后端多模块结构是这样e-commerce-backend/ ├── pom.xml # 父POM统一版本管理 ├── e-commerce-common/ # 通用类统一返回、异常、工具类 ├── e-commerce-product/ # 商品模块 ├── e-commerce-order/ # 订单模块 ├── e-commerce-user/ # 用户模块 └── e-commerce-admin/ # 管理后台接口模块父POM里做两件事用dependencyManagement锁定依赖版本用properties统一版本号。Spring Boot的版本选择非常关键热词里一直在刷“springboot版本太高”的坑3.x和2.x的差异不只是javax到jakartaredis、mybatis整合方式都有变化。做这套电商项目我建议直接用Spring Boot 2.7.x稳定网上资料多mybatis-spring-boot-starter也兼容得最好。properties java.version1.8/java.version spring-boot.version2.7.18/spring-boot.version mysql.version5.7.44/mysql.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring-boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这里有个参数说明Java版本锁在1.8不是因为新版本不好而是这套技术栈在1.8下踩坑最少。MySQL用5.7.44是因为5.7的InnoDB在事务和锁机制上与MyBatis的配合非常成熟网上遇到的案例最多排查资料也好找。Spring Boot版本一旦高于3.0javax.servlet要改成jakarta.servlet部分老版本的MyBatis分页插件直接失效这种连带问题会浪费你一整个下午。2.2 Spring Boot整合SSM数据源、MyBatis与事务配置SSM在Spring Boot里不再需要那些繁琐的XML配置但你要明白底层装配逻辑不然后面遇到“mapper注入为null”这种问题会毫无头绪。SSM指的就是Spring Spring MVC MyBatisSpring Boot通过自动配置把它们串起来。先创建数据库和表这是电商项目的地基。商品表、订单表、购物车表、用户表下面这份SQL是精简版但字段和索引思路是完整的CREATE DATABASE IF NOT EXISTS ecommerce DEFAULT CHARACTER SET utf8mb4; CREATE TABLE t_product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(128) NOT NULL COMMENT 商品名称, product_price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存, category_id BIGINT NOT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB COMMENT商品表; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL UNIQUE COMMENT 订单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB COMMENT订单表;pom.xml里加上SSM三件套的Spring Boot整合依赖注意不是单独引入spring、springmvc而是引入启动器dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependencymybatis-spring-boot-starter的版本同样要锁住2.3.x配合Spring Boot 2.7.x是经过大量项目验证的。不要用3.0的mybatis starter它的包名和自动化配置有破坏性调整。application.yml是这套项目的总控文件数据源、Redis连接、MyBatis映射、事务管理都在这里spring: datasource: url: jdbc:mysql://localhost:3306/ecommerce?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.ecommerce.entity configuration: map-underscore-to-camel-case: true这里有几个关键参数值得说明serverTimezone一定要显式设置否则MySQL 5.7以上版本会报时区错误这种错误在启动时看不出来但查询时间字段时会差8小时。Redis的timeout设成3000ms是个折中值太小容易在生产环境误报超时太大又会让接口响应变慢。max-active8是lettuce连接池的默认值电商场景下单机并发不高时够用但如果要做秒杀这个值要调大到20以上。2.3 Vue前端工程与后端联调代理和打包前端用Vue CLI初始化工程这部分对应的就是热词里的“vue安装及环境配置”和“vue路由”。电商项目前端不是重点但联调时有一个配置是必经之路就是开发环境代理。前后端分离后前端跑在8080端口后端跑在8081端口跨域问题必须通过代理解决而不是在后端代码里写CrossOrigin。vue.config.js里这样配module.exports { devServer: { port: 8080, host: localhost, proxy: { /api: { target: http://localhost:8081, changeOrigin: true, pathRewrite: { ^/api: } } } } };关键在pathRewrite这一行它的作用是把前端请求的/api前缀剥掉再转发给后端。我见过很多新手前后端联调失败不是因为Java代码有问题而是代理没配好请求打到了后端找不到的路径上。changeOrigin必须设为true否则后端拿到的Host头还是前端地址一些基于域名做权限校验的后端接口会直接拒绝。前端路由用Vue Router电商项目至少要有首页、商品列表、商品详情、购物车、订单确认、订单列表这六个页面。这个阶段不着急写业务把路由和页面骨架搭好能通过代理调到后端接口就说明工程骨架已经通了。3. 商品中心与Redis缓存把热点数据从MySQL里解放出来3.1 商品表设计的SPU与SKU电商项目的经典模型商品模块是电商项目的核心数据源。早期做电商项目的人容易把商品设计成一张大表所有属性全塞进去结果就是字段爆炸、查询缓慢、扩展困难。稍微正规一点的电商项目都会区分SPU和SKUSPU是商品聚合比如“iPhone 15 Pro”SKU是具体可购买的规格比如“iPhone 15 Pro 黑色 256GB”。这个模型直接决定了Redis缓存怎么设计。SPU信息变化频率低、访问频率高适合做缓存SKU的库存变化频率极高不适合长时间缓存。我一般把SPU整体缓存管叫“商品缓存”SKU库存单独走Redis的String类型存数字配合后面的分布式锁和事务使用。商品列表页的SQL查询在电商项目里是一个极易翻车的点。很多人一上来就SELECT *商品一多连表查分类和品牌数据库CPU直接被打满。我的习惯是列表页只查商品ID和必要展示字段详情再查全量。配合MySQL的索引商品列表的查询语句要做到explain里type至少是ref不能是ALL全表扫描。SELECT id, product_name, product_price, main_image FROM t_product WHERE category_id #{categoryId} AND status 1 ORDER BY id DESC LIMIT #{offset}, #{pageSize};这条SQL的写法有几个细节只查列表需要的字段减少传输和内存消耗ORDER BY用主键id而不是创建时间因为id的索引天然有序limit分页传offset和pageSize后端做统一分页参数对象。这样的查询在商品数量百万级以内都能保持稳定响应。3.2 用RedisTemplate还是Spring Cache缓存方案选型商品详情页是最典型的缓存应用场景。一个商品被成千上万人同时浏览如果每次请求都打MySQL数据库迟早被压垮。Redis在这里做的是“缓存治理”把热点数据的读压力从MySQL扛走。但实现方式有分歧Spring Cache的注解方式简单RedisTemplate手动方式灵活。我的建议是商品缓存用RedisTemplate手动控制。原因很简单Spring Cache的注解适合不怎么变化的缓存但商品缓存需要处理缓存空值防止穿透、需要设置不同的过期时间、需要配合布隆过滤器这些逻辑用注解表达起来非常别扭。Service public class ProductService { Autowired private StringRedisTemplate redisTemplate; Autowired private ProductMapper productMapper; private static final String PRODUCT_CACHE_KEY product:detail:; public ProductVO getProductDetail(Long productId) { // 先查缓存命中直接返回 String cached redisTemplate.opsForValue().get(PRODUCT_CACHE_KEY productId); if (cached ! null) { if (EMPTY.equals(cached)) { // 缓存空值防止缓存穿透 return null; } return JSON.parseObject(cached, ProductVO.class); } // 缓存未命中查MySQL Product product productMapper.selectById(productId); if (product null) { // 设置空值缓存过期时间设短 redisTemplate.opsForValue().set(PRODUCT_CACHE_KEY productId, EMPTY, 60, TimeUnit.SECONDS); return null; } ProductVO vo convertToVO(product); // 商品信息缓存10分钟过期时间略微随机化防雪崩 long expireTime 600 new Random().nextInt(60); redisTemplate.opsForValue().set( PRODUCT_CACHE_KEY productId, JSON.toJSONString(vo), expireTime, TimeUnit.SECONDS ); return vo; } }这段代码的逻辑说明先读缓存缓存有就直接返回JSON反序列化后的对象缓存没有就读数据库数据库也没有就把“EMPTY”这个标记写进缓存这就是缓存穿透的标准解法。过期时间600秒加随机数是为了防止大量商品同时失效导致缓存雪崩。这套逻辑用Spring Cache的Cacheable注解是实现不了这么精细的控制的。代码里的参数值得你记录空值缓存过期时间只设60秒因为空值不需要长期占用Redis内存正常商品缓存过期时间在10分钟上下浮动具体数值取决于你的商品更新频率我做过一个比价类项目商品价格变动频繁缓存时间压到了3分钟。3.3 布隆过滤器缓存穿透的最终防线商品列表和详情页的缓存穿透问题在正常流量下不会暴露一旦遇到恶意脚本用不存在的商品ID批量刷接口请求会绕过缓存直接打到MySQL。空值缓存能解决一部分问题但每次穿透都查一次数据库的成本还是高。更彻底的方案是布隆过滤器。布隆过滤器的作用就是快速判断一个ID是否可能存在。它的特点是“一定不存在的肯定不存在存在的可能误判存在”这种特性恰好适合做缓存穿透过滤。Configuration public class BloomFilterConfig { private static final int EXPECTED_SIZE 100000; private static final double FPP 0.01; Bean public BloomFilterLong productBloomFilter() { BloomFilterLong filter BloomFilter.create(Funnel.longFunnel(), EXPECTED_SIZE, FPP); // 系统启动时把商品表中的所有商品ID预加载进布隆过滤器 productMapper.selectAllIds().forEach(filter::put); return filter; } }参数说明EXPECTED_SIZE设置为10万是预估的商品总量设置小了会导致误判率急剧上升设置大了会浪费内存。FPP误判率设为1%意味着100个不存在的ID里可能有1个会穿透到数据库这个概率对于电商商品ID来说完全可接受。启动时全量把商品ID加载进布隆过滤器是一个简单但有效的初始化方式商品量到百万级时这个初始化过程要花十几秒但换来的是缓存层几乎100%的抗穿透能力。用的时候在getProductDetail方法最前面加一行判断if (!productBloomFilter().mightContain(productId)) { return null; }这一行的意思是布隆过滤器说这个ID不存在就直接返回空连Redis都不查。这就是“缓存治理”里最常见也最实用的三道防线布隆过滤器挡第一层空值缓存挡第二层MySQL兜底第三层。4. 购物车与订单MySQL事务、库存扣减与Redis分布式锁4.1 购物车用Redis存储还是MySQL存储购物车是电商项目里一个很有意思的设计点。用MySQL存储好处是持久化、跨设备同步坏处是用户每次加购都要写库频繁的读写会让数据库压力山大。用Redis存储好处是读写极快、天然过期坏处是Redis重启会丢数据用户的购物车可能莫名其妙消失。折中方案是这样购物车以Redis存储为主用户登录后把购物车数据异步同步一份到MySQL。Redis里购物车的key用hash类型field是商品IDvalue是加购数量。这样设计的好处是用户改数量时不需要读整个购物车直接操作Redis的hash字段。Service public class CartService { Autowired private StringRedisTemplate redisTemplate; private static final String CART_KEY_PREFIX cart:user:; // 加购操作 public void addToCart(Long userId, Long productId, Integer count) { String cartKey CART_KEY_PREFIX userId; // hash操作field是商品IDvalue是数量 redisTemplate.opsForHash().increment(cartKey, productId.toString(), count); // 设置过期时间30天不登录清空购物车避免Redis内存泄漏 redisTemplate.expire(cartKey, 30, TimeUnit.DAYS); } // 查询购物车 public MapObject, Object getCart(Long userId) { String cartKey CART_KEY_PREFIX userId; MapObject, Object entries redisTemplate.opsForHash().entries(cartKey); // 这部分可以异步加载商品详情补充购物车展示信息 return entries; } }Redis的hash结构做购物车有一个天然的优势increment命令是原子的用户连续点击加购不会出现数量覆盖丢失的问题。过期时间设30天是平衡用户体验和Redis内存的常用做法太短用户久了回来购物车空了体验差太长Redis里全是僵尸数据。这里你要注意一个坑购物车数据在Redis里只能存商品ID和数量不能存商品价格。原因很简单商品价格会变购物车显示的价格应该在下单时实时从商品服务取而不是用用户在购物车页面看到的历史价格。4.2 订单事务与库存扣减MySQL事务和锁的配合下单流程是整个电商项目技术含量最高的部分涉及的事务有三种订单创建、库存扣减、购物车清理。这三个操作必须在一个事务里完成任何一步失败都要回滚。Spring Boot里用Transactional注解是最直接的方案。Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private ProductMapper productMapper; Transactional(rollbackFor Exception.class) public Order createOrder(OrderRequest request) { // 1. 生成订单号用雪花算法或者时间戳随机数 String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(request.getUserId()); order.setTotalAmount(calculateAmount(request)); // 2. 扣减库存UPDATE加条件判断防止超卖 int updated productMapper.deductStock(request.getProductId(), request.getCount()); if (updated 0) { throw new BusinessException(库存不足); } // 3. 创建订单 orderMapper.insert(order); // 4. 清理购物车对应商品 cartService.removeItem(request.getUserId(), request.getProductId()); return order; } }这里最关键的是第2步库存扣减。如果你的SQL写的是先查库存再判断够不够然后UPDATE库存减一那这个接口在并发100时一定会超卖因为读和写之间有时间差。正确做法是直接把判断条件写进UPDATE语句中UPDATE t_product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}这条UPDATE的含义是只有当库存大于等于购买数量时才执行扣减。MySQL的UPDATE是行锁级别的原子操作两个并发请求同时走这条SQL时只有一个能更新成功另一个受影响行数为0。通过受影响行数判断库存是否扣减成功是MySQL层面的最优解比Java层加锁效率高得多。事务的rollbackForException.class这个参数值得特意说明Spring默认只在遇到RuntimeException时才回滚checked exception不会触发回滚。你在事务方法里抛一个IOException默认情况下之前的数据库操作照样提交。所以事务方法必须显式声明rollbackFor为Exception.class这是血泪经验。4.3 Redis分布式锁避免库存超卖的另一种解法MySQL的乐观锁UPDATE能解决单库库存扣减但电商项目里的库存往往还有Redis里的预扣库存尤其是接入秒杀场景后。热词里一直在刷“redis分布式锁”到这一步真实需要一个能把Redis里的库存安全扣减的方案。它的核心是多个线程同时修改Redis里的库存值必须保证只有一个线程能执行扣减操作。Redis分布式锁的经典实现是SETNX加过期时间但这里面坑非常多。下面是落地版本的代码public boolean tryLock(String lockKey, String requestId, long expireSeconds) { // SET key value NX EX 是原子操作分布式锁的根 Boolean result redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); } public void unlock(String lockKey, String requestId) { // 用Lua脚本保证“判断是自己持有锁删除锁”是原子的 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong redisScript new DefaultRedisScript(script, Long.class); redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId); }这段代码里有两个参数是绝对不能省的一个是requestId它必须是当前线程的唯一标识比如UUID作用是在释放锁时确认这把锁确实是自己的防止误删了其他线程的锁一个是expireSeconds必须给锁设置过期时间否则持有锁的线程挂了锁永远不会释放造成死锁。为什么释放锁要单独用Lua脚本因为“检查锁是否是自己持有”和“删除锁”是两个操作如果分开执行中间可能有别的线程抢占了锁。用Lua脚本保证这两条命令的原子性是我在这个项目里踩坑后学到的。网上有很多热词帖子讲Redis分布式锁的各种翻车案例大多数翻车原因就是忽略了requestId的校验或过期时间。库存扣减时的使用方式是把分布式锁和MySQL扣减结合起来先锁住Redis里的库存再走事务扣减MySQL库存public void deductStockWithLock(Long productId, Integer count) { String lockKey lock:product: productId; String requestId UUID.randomUUID().toString(); boolean locked tryLock(lockKey, requestId, 10); if (!locked) { throw new BusinessException(当前操作人数过多请稍后重试); } try { // 获锁后做库存等数据的一致性校验和扣减 createOrder(request); } finally { unlock(lockKey, requestId); } }5. 电商项目常见问题排查从启动失败到缓存穿透的5条踩坑记录5.1 Spring Boot版本太高导致Redis连接失败现象项目用Spring Boot 3.x启动Redis连接一直报错控制台提示“Unable to connect to Redis”但Redis进程明明是正常运行的。想回退版本发现代码里javax.servlet相关的import全部报错。原因Spring Boot 3.x把javax命名空间迁移到了jakartalettuce客户端的一些底层配置也变了。很多老版本的redis客户端依赖不兼容3.x的自动配置机制。更隐蔽的是3.x对Redis连接池的默认配置做了调整很多2.x时代的配置项失效或被忽略。解决电商项目不追求新版本直接用Spring Boot 2.7.18 mybatis-spring-boot-starter 2.3.2 mysql-connector-java 5.7.44这一套组合。这不是技术保守是这套版本组合经过了海量项目的验证遇到过的问题都能在网上找到现成的排查方案。5.2 前端Vue打包放进Spring Boot后页面空白现象开发环境跑得好好的npm run build之后把dist目录扔进Spring Boot的static文件夹启动后访问首页一片空白F12控制台显示404或者加载不到JS文件。原因Vue默认的publicPath是/打包后的JS和CSS用的是绝对路径部署到Spring Boot后如果项目不是在根路径下启动路径就对不上。另一个常见原因是Vue Router开启了history模式nginx或Spring Boot没有配置路径重写规则刷新页面时直接404。解决vue.config.js里把publicPath改成./这样打包资源就变成了相对路径Vue Router用hash模式别开history省去服务端配置的麻烦。这是我做前后端分离项目最省心的部署组合代价是URL里会带一个#号但电商后台管理系统完全不介意这个。5.3 Docker安装MySQL失败但宿主机MySQL正常现象本地用Docker启动MySQL容器端口也映射了但Spring Boot连接时总报Access denied或者连接超时。查了容器日志MySQL初始化没有正常完成。原因MySQL 5.7的Docker容器首次启动时要执行初始化脚本如果你没有设置MYSQL_ROOT_PASSWORD环境变量或者挂载了本地数据目录却用了不同的字符集容器会启动失败。还有一种翻车情况是端口映射用了3306:3306但宿主机自带的MySQL已经占用了3306端口。解决给MySQL容器单独映射一个端口比如3336:3306避免和宿主机端口冲突。启动命令里必须带上MYSQL_ROOT_PASSWORD。修改Spring Boot的数据源URL端口为3336。这条经验能帮你省掉至少半小时的排查时间。5.4 Redis缓存穿透商品ID被恶意循环请求打垮现象某天MySQL的CPU突然飙高慢查询日志里全是同一个SQL在查不存在的商品ID。Redis命中率从95%降到了40%说明大量请求穿过了缓存打到数据库。原因有人用脚本循环调用商品详情接口传的商品ID都是不存在的。因为没有布隆过滤器空值也没有缓存请求全部穿透到MySQL。600秒的缓存对不存在的ID不起作用每次请求都执行了一次数据库SELECT。解决加布隆过滤器前置过滤加空值缓存兜底同时给商品详情接口加了简单的接口限流同一IP每秒超过20次直接返回错误码。布隆过滤器挡住99%的无效请求空值缓存挡住剩余的1%MySQL的压力瞬间就下来了。5.5 分布式锁释放时误删了别人的锁现象高并发下单时段用户A的订单莫名其妙被取消日志里显示“未持有锁却执行了释放锁操作”。查代码发现释放锁时只用了删除key没有判断key的value。原因线程A获取锁后执行时间过长锁自动过期了线程B获取锁成功开始执行线程A执行完毕后直接DELETE了锁key把线程B的锁删掉了。这导致线程B和线程C同时进入临界区库存扣减失去保护。解决释放锁时用Lua脚本检查value是否为当前线程的requestId是才删除。修改后误删锁的问题再没出现过。既然锁设了过期时间就必须在释放时校验持有者身份这个流程里没有一个参数是多余的。6. 支付回调的幂等处理电商项目上线前最该补的一课订单创建完项目还没有真正闭环支付回调才是检验一个电商项目是否成熟的分水岭。微信或支付宝的支付回调会以异步通知方式发送到你配置的回调地址一个关键特点是同样的通知可能发送多次必须做到幂等处理即重复通知不会产生重复订单或重复加余额。幂等的标准做法是在订单表加一个唯一约束的字段也就是前面设计表中的order_no做了UNIQUE。支付回调里先按order_no查询订单如果订单状态已经是“已支付”直接返回成功不再重复处理。这是最基础的一层防线。更稳的是加一张支付回调记录表每次回调先尝试insert这条通知记录如果主键冲突说明已经处理过直接返回成功。上线前的验证方法我习惯分三步一是用Postman或curl手动模拟支付回调请求重复发送三次同样的通知看订单状态是否被正确置为“已支付”是否产生任何重复数据二是把MySQL的慢查询日志开着在并发下单测试时观察有没有全表扫描的SQL偷摸跑出来三是Redis里那几个核心缓存的命中率用info命令盯着看命中率低于90%就要回头检查缓存策略。我最早做电商项目时根本没想过幂等这回事直到一次测试环境里支付回调重放了十几遍订单表出现了十几条重复数据才明白微信文档里为什么反复强调幂等。后来我养成了一个习惯任何涉及到钱的接口第一版代码就要把幂等校验写进去后来在面试里讲这个点比背一堆框架面试题管用得多。这套电商项目做到支付回调这步能顺利跑通基本就具备上线能力了剩下的就是按真实流量慢慢调Redis缓存参数和MySQL索引。希望帮到你。本文还有配套的精品资源点击获取