SpringBoot+Mybatis-Plus+Vue+MySQL工程化落地实战

📅 发布时间:2026/10/15 7:13:29
SpringBoot+Mybatis-Plus+Vue+MySQL工程化落地实战
1. 这不是“又一个整合教程”而是一套能直接上线的工程化落地方案你搜“SpringBootMybatis-plusMysqlVue整合”页面上铺天盖地是那种——先建个SpringBoot空项目再加个mybatis-plus依赖写个User实体类贴两段Mapper XML前端用Vue写个列表页最后截图展示“Hello World”式表格。这种内容我十年前就写过现在看连调试日志都懒得配全。真正卡住团队进度的从来不是“能不能跑起来”而是“怎么让这套组合在真实业务里不崩、不慢、不漏、不被安全扫描器标红”。我带过的三个中型后端团队平均每个项目在整合层踩过至少7类典型坑跨域配置写死在开发环境导致测试环境403Mybatis-Plus的自动填充字段在分布式事务里时间戳错乱Vue打包后静态资源路径和Nginx反向代理规则打架MySQL连接池参数没调高峰期连接耗尽报Cannot create PoolableConnectionFactory还有更隐蔽的——Mybatis-Plus的TableField(fill FieldFill.INSERT)在批量插入时根本不起作用数据缺省值全成null等上线查账才发现金额字段大面积丢失。这个标题里的四个技术栈本质是四层责任边界SpringBoot是容器与生命周期管理者Mybatis-Plus是数据访问契约执行者MySQL是状态存储的物理载体Vue是用户意图的可视化翻译器。它们之间不是简单拼接而是存在三处关键耦合点API契约一致性后端返回的JSON结构必须与前端Vuex store定义的state严格对齐、数据流时效性控制比如订单状态变更Vue需要实时感知但Mybatis-Plus默认的二级缓存可能让前端看到过期数据、错误传播链完整性MySQL唯一索引冲突要能穿透Mybatis-Plus异常封装最终在Vue界面上精准提示“手机号已被注册”而不是弹出一串DuplicateKeyException堆栈。我接下来要拆解的就是这三处耦合点的工程化解法所有代码片段都来自某电商平台后台管理系统的V2.3版本已稳定运行14个月日均处理订单请求27万次。你不需要从零开始搭架子只需要把对应模块的配置和逻辑抄过去就能避开90%的线上事故。2. 整体架构设计与技术选型逻辑2.1 为什么放弃XML而坚持注解驱动——Mybatis-Plus的“契约即代码”哲学很多教程还在教你怎么写UserMapper.xml甚至配resultMap映射复杂嵌套对象。这在2024年已经属于反模式。Mybatis-Plus的核心价值从来不是简化SQL编写而是把数据访问契约固化在Java代码里。当你用TableName(sys_user)标注实体类用TableId(type IdType.ASSIGN_ID)声明主键策略用TableField(value create_time, fill FieldFill.INSERT)定义自动填充字段——这些注解共同构成了一个可编译、可校验、可版本管理的契约文档。它比任何XML或Swagger文档都可靠因为IDE能实时检查TableField的value是否与数据库列名一致Maven编译阶段就能发现fill策略与实际业务逻辑的矛盾比如你在更新操作里误用了FieldFill.INSERT。我见过最典型的反例某团队为支持多租户在XML里硬编码WHERE tenant_id #{tenantId}结果上线后因租户ID传参为空SQL变成WHERE tenant_id null全库数据被误删。而如果采用注解驱动租户ID过滤应该通过MetaObjectHandler实现全局拦截或者用Interceptor注入自定义分页插件把租户隔离逻辑下沉到框架层业务代码里根本看不到tenant_id这个词。这正是Mybatis-Plus的设计哲学——让开发者只关注业务语义把基础设施细节交给框架契约约束。2.2 Vue为何必须采用Composition API Pinia——告别“数据流向不可控”的前端黑洞老式Vue2的Options API配合Vuex最大的隐患是数据流向像迷宫。一个按钮点击触发this.$store.dispatch(user/fetchList)然后在actions里调用API再commit mutation更新state最后组件watch这个state变化。整个链条里你根本不知道fetchList到底会触发多少个mutation也不知道某个mutation会不会意外修改了其他模块的state。当项目超过5万行代码这种不可控性会指数级放大。Composition API Pinia的组合强制你把数据流切成原子化单元。比如用户管理模块我会创建useUserStore.tsexport const useUserStore defineStore(user, () { const list refUserItem[]([]) const loading ref(false) const total ref(0) const fetchList async (params: UserQueryParams) { loading.value true try { const res await api.user.getList(params) // 直接调用封装好的API函数 list.value res.data.records total.value res.data.total } finally { loading.value false } } return { list, loading, total, fetchList } })注意这里没有commit没有dispatch只有明确的list.value ...赋值。Pinia的state是响应式refmutation就是直接赋值action就是异步函数。这种设计让数据流向变成一条直线用户操作 → 调用action → API返回 → 更新state → 视图自动刷新。当出现数据不更新的问题你只需要检查fetchList函数里list.value的赋值时机而不用在Vuex的devtools里翻几十层mutation记录。我在某SaaS后台项目里实测采用此方案后前端数据相关bug的平均定位时间从47分钟缩短到6分钟。2.3 MySQL连接池为何必须用HikariCP——不只是性能更是故障隔离能力Spring Boot 2.0之后默认集成HikariCP但很多团队仍手动替换成Druid理由是“Druid有监控页面”。这是典型的本末倒置。HikariCP的核心优势不在连接获取速度虽然它确实快而在于连接健康度的主动探测机制。它会在连接归还到池子时执行一条轻量级的isValid()检测默认是SELECT 1如果检测失败该连接会被立即丢弃不会污染整个连接池。而Druid的检测是被动的——只有当应用尝试使用连接时才会发现Connection closed异常此时错误已经传播到业务层可能已经触发了多次重试甚至引发雪崩。更关键的是HikariCP的leakDetectionThreshold参数。我们在线上环境设置为60000毫秒即连接被借出超过60秒未归还HikariCP会主动打印堆栈日志精准定位哪段代码忘了关闭SqlSession。这个功能救过我们三次大忙一次是定时任务里Mybatis-Plus的LambdaQueryWrapper构造错误导致查询条件永远为真全表扫描锁表另一次是前端上传大文件时后端流式解析CSV忘记在try-with-resources里关闭数据库连接。如果没有这个泄漏检测问题会持续数小时才被DBA告警发现。而HikariCP的日志里第一行就写着Connection leak detection triggered for connection com.mysql.cj.jdbc.ConnectionImplxxxx, stack trace follows后面跟着完整的调用栈运维同学照着日志就能直接找到问题代码行。3. 核心模块实现与关键配置详解3.1 后端SpringBoot Mybatis-Plus 的生产级配置3.1.1 数据源配置——绕不开的SSL与字符集陷阱很多团队在application.yml里只写spring: datasource: url: jdbc:mysql://localhost:3306/mydb username: root password: 123456这在本地开发没问题但一上生产环境必然出事。MySQL 8.0默认要求SSL连接而上述配置会静默降级为非SSL导致连接池初始化失败。正确写法必须显式声明SSL策略spring: datasource: url: jdbc:mysql://prod-db:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4allowPublicKeyRetrievaltrue username: app_user password: ${DB_PASSWORD:default_pass} hikari: connection-timeout: 30000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000 maximum-pool-size: 20 minimum-idle: 5 leak-detection-threshold: 60000 # 关键强制验证连接有效性 connection-test-query: SELECT 1注意几个致命参数useSSLfalse生产环境若未配置SSL证书必须显式关闭否则HikariCP会卡在连接初始化characterEncodingutf8mb4这是硬性要求utf8在MySQL里实际是utf8mb3不支持emoji和部分生僻汉字线上用户昵称含表情符号时必现乱码allowPublicKeyRetrievaltrueMySQL 8.0的RSA密钥交换机制需要此参数否则Access denied for user错误无从排查leak-detection-threshold: 60000如前所述这是连接泄漏的救命稻草。提示DB_PASSWORD使用占位符${DB_PASSWORD:default_pass}避免密码硬编码。K8s部署时通过Secret挂载环境变量Docker Compose则用.env文件注入。3.1.2 Mybatis-Plus 全局配置——让框架替你思考边界Mybatis-Plus的MybatisPlusConfig类常被写成空壳只注册一个MybatisPlusAutoConfiguration。这浪费了框架最强大的能力。真正的生产配置必须包含三处核心拦截Configuration MapperScan(com.example.mapper) public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 1. 分页插件 - 必须放在第一位 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 2. 乐观锁插件 - 防止并发更新覆盖 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 3. 多租户插件 - 租户ID自动注入WHERE条件 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { Override public Expression getTenantId() { // 从ThreadLocal获取当前租户ID由网关统一注入 Long tenantId TenantContext.getTenantId(); return new LongValue(tenantId); } Override public String getTenantIdColumn() { return tenant_id; } Override public boolean ignoreTable(String tableName) { // 系统表不走租户隔离 return Arrays.asList(sys_user, sys_role).contains(tableName); } })); return interceptor; } // 4. 自动填充处理器 - 统一管理创建/更新时间 Bean public MyMetaObjectHandler myMetaObjectHandler() { return new MyMetaObjectHandler(); } }其中MyMetaObjectHandler的实现尤为关键Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { // 创建时间优先取数据库默认值若无则用当前时间 this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); // 字段名称, 属性类型, 默认值 this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, createBy, Long.class, SecurityUtils.getUserId()); // 当前登录用户ID } Override public void updateFill(MetaObject metaObject) { // 更新时间必须每次都刷不能用默认值 this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, updateBy, Long.class, SecurityUtils.getUserId()); } }注意strictInsertFill和strictUpdateFill方法会严格检查字段是否存在、类型是否匹配避免因字段名拼写错误导致填充失效。SecurityUtils.getUserId()需自行实现通常从JWT Token或Session中解析用户ID。3.1.3 实体类设计规范——用注解定义业务契约以用户实体为例User.java必须体现三层契约TableName(sys_user) Data EqualsAndHashCode(callSuper false) Accessors(chain true) // 支持链式调用 public class User implements Serializable { private static final long serialVersionUID 1L; TableId(type IdType.ASSIGN_ID) // 雪花算法ID避免数据库主键暴露业务量 private Long id; TableField(username) NotBlank(message 用户名不能为空) Size(max 32, message 用户名长度不能超过32位) private String username; TableField(phone) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; TableField(status) TableLogic // 逻辑删除字段查询时自动追加 WHERE status ! 1 private Integer status; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableField(exist false) // 非数据库字段仅用于传输 private ListRole roles; }关键点解析TableLogic启用Mybatis-Plus的逻辑删除比物理删除更安全且selectList等方法会自动过滤已删除数据TableField(exist false)明确标识DTO字段避免Mybatis-Plus试图映射不存在的列导致SQL错误NotBlank、PatternJSR-303校验注解配合Valid在Controller层做前置校验比在Service里if-else判断更优雅。3.2 前端Vue 3 Composition API 的工程化实践3.2.1 API 请求层封装——终结“每个组件都写axios”新手常犯的错误是在每个Vue组件里直接写axios.get(/api/user/list)。这导致三个问题URL硬编码无法统一管理、错误处理重复、Token注入不一致。正确的做法是建立分层API体系第一步定义基础请求配置// src/utils/request.ts import axios from axios import { ElMessage } from element-plus // 创建axios实例 const request axios.create({ baseURL: import.meta.env.VUE_APP_BASE_API, // 从环境变量读取 timeout: 10000, }) // 请求拦截器 request.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) // 响应拦截器 request.interceptors.response.use( (response) { const { code, data, msg } response.data if (code 200) { return data // 直接返回data业务层无需解包 } else { ElMessage.error(msg || 请求失败) return Promise.reject(new Error(msg)) } }, (error) { if (error.response?.status 401) { // Token过期跳转登录页 localStorage.removeItem(token) window.location.href /login } ElMessage.error(error.message) return Promise.reject(error) } ) export default request第二步按模块组织API函数// src/api/user.ts import request from /utils/request export interface UserItem { id: number username: string phone: string status: number } export interface UserQueryParams { page: number size: number username?: string } export interface UserPageResponse { records: UserItem[] total: number size: number current: number } // 用户管理API export const userApi { getList: (params: UserQueryParams) request.getUserPageResponse(/user/page, { params }), getById: (id: number) request.getUserItem(/user/${id}), save: (data: PartialUserItem) request.post(/user, data), deleteById: (id: number) request.delete(/user/${id}) }第三步在Pinia Store中调用// src/stores/user.ts import { defineStore } from pinia import { userApi, UserItem, UserQueryParams, UserPageResponse } from /api/user export const useUserStore defineStore(user, () { const list refUserItem[]([]) const loading ref(false) const total ref(0) const currentPage ref(1) const pageSize ref(10) const fetchList async (params: UserQueryParams) { loading.value true try { const res await userApi.getList(params) list.value res.records total.value res.total currentPage.value res.current pageSize.value res.size } finally { loading.value false } } return { list, loading, total, currentPage, pageSize, fetchList } })这样设计的好处是URL集中管理、Token自动注入、错误统一处理、类型安全TS接口约束、业务逻辑与网络请求完全解耦。当后端API路径变更时只需修改user.ts里的字符串所有调用处自动生效。3.2.2 表单验证与提交——用Element Plus的Rules实现零代码校验Vue组件中的表单验证很多人手写一堆if (!this.form.username) { this.errors.username 用户名必填 }。Element Plus的rules配置可以彻底解放双手template el-form :modelform :rulesrules refformRef el-form-item label用户名 propusername el-input v-modelform.username / /el-form-item el-form-item label手机号 propphone el-input v-modelform.phone / /el-form-item el-form-item el-button typeprimary clicksubmitForm提交/el-button /el-form-item /el-form /template script setup langts import { ref, reactive } from vue import { ElMessage } from element-plus const formRef ref() const form reactive({ username: , phone: }) // 定义校验规则 const rules { username: [ { required: true, message: 用户名不能为空, trigger: blur }, { min: 2, max: 32, message: 长度在 2 到 32 个字符, trigger: blur } ], phone: [ { required: true, message: 手机号不能为空, trigger: blur }, { pattern: /^1[3-9]\d{9}$/, message: 请输入正确的手机号, trigger: blur } ] } const submitForm async () { try { await formRef.value.validate() // 触发校验 // 校验通过提交表单 await userApi.save(form) ElMessage.success(保存成功) } catch (error) { console.log(校验失败, error) } } /scriptElement Plus的validate()方法会遍历所有prop字段根据rules配置执行校验并自动在对应el-form-item下显示错误提示。你不需要写一行校验逻辑代码所有规则都在rules对象里声明维护成本极低。4. 全链路联调与常见问题实战排查4.1 跨域问题不止是CrossOrigin那么简单开发阶段最常见的403错误表面是跨域根源往往是预检请求OPTIONS被拦截。很多人只在Controller上加CrossOrigin却忽略了Spring Security的默认配置会拦截OPTIONS请求。正确解法分三步第一步配置CORS全局策略Configuration EnableWebMvc public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8080, https://admin.example.com) // 明确指定域名禁用* .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) // 必须包含OPTIONS .allowCredentials(true) // 若需携带Cookie必须设为true .maxAge(3600); } }第二步确保Spring Security放行OPTIONSConfiguration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) // 启用CORS配置 .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authz - authz .requestMatchers(HttpMethod.OPTIONS, /**).permitAll() // 关键放行所有OPTIONS .requestMatchers(/login, /public/**).permitAll() .anyRequest().authenticated() ); return http.build(); } }第三步Vue开发服务器代理配置vite.config.tsexport default defineConfig({ server: { proxy: { /api: { target: http://localhost:8081, // 后端地址 changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) // 去掉/api前缀 } } } })这样配置后前端请求/api/user/list会被Vite代理到http://localhost:8081/user/list完全规避浏览器跨域限制且无需后端CORS配置。生产环境则由Nginx统一处理。4.2 MySQL连接池耗尽从日志定位根因当系统突然大量报Cannot get JDBC Connection不要急着重启服务。先看HikariCP日志[HikariPool-1 housekeeper] WARN com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta5m23s876ms). [HikariPool-1 connection adder] WARN com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Connection add failed. java.sql.SQLTimeoutException: Timeout after 30000ms of waiting for a connection.日志里两个关键线索Thread starvation线程饥饿说明CPU或IO严重瓶颈Timeout after 30000ms连接获取超时。排查步骤检查数据库连接数show status like Threads_connected;看是否接近max_connections上限检查慢查询show processlist;找出State为Sending data或Locked的长事务检查应用线程jstack pid | grep java.lang.Thread.State: BLOCKED看是否有线程阻塞在数据库操作上检查HikariCP指标通过Actuator端点/actuator/metrics/hikaricp.connections.active查看活跃连接数。我们曾遇到一个典型案例某报表导出接口Mybatis-Plus的selectList查询返回10万条数据前端未做分页导致JVM内存溢出GC频繁最终HikariCP无法创建新连接。解决方案是强制分页流式导出后端改用selectPage前端用a href/api/export?paramsxxx触发下载避免大数据量加载到内存。4.3 Vue页面白屏检查Source Map与路由守卫Vue打包后白屏90%的原因是路由路径错误router/index.ts里base配置与Nginxlocation不匹配静态资源404index.html里引用的js/chunk-xxx.js返回404Source Map缺失生产环境未生成Source Map无法定位错误。Nginx配置必须与Vue Router的history模式严格对应location / { try_files $uri $uri/ /index.html; # 关键所有路径都 fallback 到 index.html } # 若Vue部署在子路径如 https://example.com/admin/ location /admin/ { alias /var/www/admin/; try_files $uri $uri/ /admin/index.html; }同时vite.config.ts中必须配置export default defineConfig({ build: { sourcemap: true, // 生产环境生成Source Map rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router, pinia, element-plus] } } } } })当白屏发生时打开浏览器开发者工具查看Network标签确认index.html、main.js、vendor.js是否全部200查看Console标签是否有Uncaught SyntaxError或router is not defined查看Sources标签能否展开Source Map看到原始Vue文件。4.4 Mybatis-Plus批量插入失效FieldFill.INSERT的陷阱TableField(fill FieldFill.INSERT)在saveBatch方法中不生效这是Mybatis-Plus的已知行为。因为saveBatch底层调用的是INSERT INTO ... VALUES (),(),()而自动填充是在insert单条SQL时通过MetaObjectHandler注入的。解决方案有两种改用saveOrUpdateBatch它会对每条记录单独执行insert或update自动填充生效手动填充后再批量插入ListUser users ... // 原始数据 users.forEach(user - { user.setCreateTime(LocalDateTime.now()); user.setUpdateTime(LocalDateTime.now()); user.setCreateBy(SecurityUtils.getUserId()); }); userMapper.insertBatch(users); // 自定义Mapper方法自定义Mapper方法需在XML中实现!-- UserMapper.xml -- insert idinsertBatch parameterTypejava.util.List INSERT INTO sys_user (username, phone, create_time, update_time, create_by) VALUES foreach collectionlist itemitem separator, (#{item.username}, #{item.phone}, #{item.createTime}, #{item.updateTime}, #{item.createBy}) /foreach /insert注意insertBatch方法需在Mapper接口中声明并确保XML文件路径正确。这是Mybatis-Plus不支持的场景必须回归原生Mybatis能力。5. 安全加固与生产环境最佳实践5.1 SQL注入防护Mybatis-Plus的“安全边界”在哪Mybatis-Plus默认使用#{}占位符天然防SQL注入但开发者常在Select注解里手写SQL或用QueryWrapper的like方法拼接字符串这就打开了风险缺口。绝对禁止的写法// ❌ 危险字符串拼接 queryWrapper.like(username, % username %); // ❌ 更危险Select中直接拼接 Select(SELECT * FROM sys_user WHERE username LIKE %${username}%) ListUser selectByUsername(Param(username) String username);安全写法// ✅ 使用Mybatis-Plus内置方法自动转义 queryWrapper.like(username, username); // 框架自动添加% // ✅ 若必须动态SQL用bind标签 Select(script SELECT * FROM sys_user where if testusername ! null and username ! \\ AND username LIKE CONCAT(%, #{username}, %) /if /where /script) ListUser selectByUsername(Param(username) String username);Mybatis-Plus的like方法会将参数值作为预编译参数传递即使username是 OR 11最终SQL也是WHERE username LIKE ?问号处绑定的值是字面量 OR 11不会触发注入。5.2 XSS防护Vue模板中的“信任边界”Vue的v-html指令会直接渲染HTML是XSS高危区。很多团队在富文本编辑器场景下滥用v-html导致存储型XSS。安全方案后端过滤使用Jsoup库清洗HTMLString safeHtml Jsoup.clean(dirtyHtml, Whitelist.relaxed() // 白名单模式 .addTags(p, br, strong, em) // 只允许这些标签 .addAttributes(p, style) // p标签只允许style属性 .addProtocols(a, href, http, https)); // a标签href只允许http/https协议前端二次校验在Vue组件中用DOMPurify净化import DOMPurify from dompurify const cleanHtml DOMPurify.sanitize(dirtyHtml, { ALLOWED_TAGS: [p, br, strong, em], ALLOWED_ATTR: [style] }) // 在模板中使用 div v-htmlcleanHtml/div这样双重防护即使后端过滤漏掉前端也能兜底。5.3 敏感信息脱敏日志与接口响应中的“数据隐身术”生产环境日志里打印用户手机号、身份证号是重大安全隐患。Mybatis-Plus提供TableField(select false)可屏蔽字段查询但日志中仍可能泄露。全链路脱敏方案日志脱敏用Logback的MaskingPatternLayoutappender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LoggingEventCompositeJsonEncoder providers timestamp/ pattern pattern{level:%level,message:%message,thread:%thread,class:%logger{0}}/pattern /pattern json json.../json /json !-- 添加手机号脱敏 -- masking maskphone:(\d{3})\d{4}(\d{4})/mask replacementphone:$1****$2/replacement /masking /providers /encoder /appender接口响应脱敏用Jackson注解public class User { private String username; JsonSerialize(using PhoneSerializer.class) // 自定义序列化器 private String phone; // ... } public class PhoneSerializer extends JsonSerializerString { Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value ! null value.length() 11) { gen.writeString(value.substring(0, 3) **** value.substring(7)); } else { gen.writeString(value); } } }这样无论日志还是API响应手机号都自动显示为138****1234无需业务代码干预。6. 性能压测与容量规划实录6.1 压测准备用JMeter模拟真实流量我们用JMeter对用户管理接口进行压测目标QPS 500持续10分钟。关键配置线程组100个线程Ramp-Up时间为10秒循环次数100次HTTP请求GET /api/user/page?page1size20HTTP头管理器添加Authorization: Bearer xxx聚合报告监控90%Line、错误率、吞吐量。压测中发现两个瓶颈数据库连接池打满HikariCP活跃连接数达20等待连接线程数飙升Mybatis-Plus二级缓存命中率低Redis缓存命中率仅32%大量请求穿透到DB。6.2 优化措施与效果对比优化项操作QPS提升错误率下降HikariCP调优maximum-pool-size从20→30connection-timeout从30s→10s120%从3.2%→0.1%Mybatis-Plus二级缓存开启CacheNamespace设置evictionLRUflushInterval6000085%无变化缓存本身不解决错误SQL优化为username字段添加索引EXPLAIN确认走索引40%无变化最终结果QPS稳定在58090%响应时间200ms错误率0.08%。关键经验是缓存不是银弹必须配合连接池调优和SQL优化才能发挥最大效益。单纯加缓存当缓存失效时DB压力会瞬间翻倍。6.3 容量规划如何预估服务器配置根据压测数据我们推算生产环境配置QPS 500→ 每秒500次请求**