
1. 项目概述eladmin 的数据库底座到底藏着什么eladmin 这套基于 Spring Boot 2.x Spring Security MyBatis Plus Vue 的前后端分离后台权限管理系统在 GitHub 上火了这么多年很多团队直接拿它当脚手架接业务。但大部分人对它的关注点都停留在开箱即用的权限框架和代码生成器上真正决定系统能不能扛住业务的其实是那套 MySQL 库表设计。我自己用 eladmin 做过三个项目最早是拿来做个内部管理系统后来接了个面向几百家商户的运营后台。第一个项目因为完全没动表结构上线三个月后查询开始明显变慢第二个项目提前把索引、缓存、慢 SQL 都梳理了一遍同样规模的业务跑了大半年都没怎么卡过。区别在哪里就在数据库设计与优化这个环节上。这篇文章不讲怎么搭环境也不抄官方文档只聊我实际踩过的坑和验证过的优化手段。无论你只是想把 eladmin 跑起来做内部工具还是打算以它为底座接正式业务下面这些关于表结构、索引策略、缓存与慢 SQL 排查的内容都能让你少走不少弯路。2. 表结构设计拆解从权限模型到字段规范的细节与取舍2.1 RBAC 权限模型落地时的三个关键设计决策eladmin 的数据库核心是一套标准的 RBAC基于角色的访问控制模型表数量不算多但每张表的设计都有它特定的业务背景。先看核心表的角色划分表名职责关键字段我关注的点sys_user用户主表id, username, password, dept_id部门外键设计直接决定数据权限过滤方式sys_role角色表id, name, level, data_scopelevel 字段用于控制角色层级data_scope 决定数据范围sys_menu菜单/权限表id, name, pid, type, permissiontype 区分目录/菜单/按钮permission 对应后端权限标识sys_dept部门表id, name, pid, sub_count树形结构子节点数量冗余字段sys_log操作日志表id, description, request_method, time, ip最容易膨胀的表要单独设计归档策略这里我特别想聊三个容易被忽略的设计决策。第一sys_user 和 sys_role 之间的关联是典型的中间表模型。eladmin 用的 sys_users_roles 表只存 user_id 和 role_id 两个字段。这个设计保持了纯粹的 RBAC 关系避免了冗余字段导致的数据不一致问题。但实际开发中很多人直接在 sys_user 上加 role_id 字段当时看是省事后患却很大一个用户多个角色时要么拆行要么存逗号分隔的字符串查询和授权的复杂度都会爆炸。我在第二个项目里就要求团队必须遵循 eladmin 原有的中间表设计不许私自改。第二sys_menu 的 type 字段目录、菜单、按钮三级是权限控制精细度的基础。很多团队做后台只做到菜单级权限按钮级的控制靠前端 v-if 判断后端接口不设防。eladmin 这种设计把按钮也定义为一种资源配合后端 PreAuthorize 注解才能真正做到接口级权限收敛。我接手过一个项目前端把编辑按钮藏了但接口没权限校验懂点技术的用户直接通过浏览器开发者工具就能调用数据库里的数据差点被改乱。第三sys_dept 的树形结构没有用传统的 parent_id 递归查询而是配合了 sub_count 冗余字段、ancestors 字段和基于 pid 的嵌套模型。这种设计在查询某个部门及其子部门的数据权限时很高效不需要递归查询每个子节点但代价是更新节点时必须同步维护这些冗余信息。eladmin 里这部分逻辑在它的业务 Service 层封装好了如果自己改造表结构很容易把树形关系改出环。2.2 字段类型与命名规范为什么这些细节不能将就eladmin 的表结构设计在字段类型上有一套自己的逻辑初看没啥特别细看会发现很多值得保留的习惯。主键统一用 bigint 而非 int。这个决策在数据量过千万时会区分很明显。int 最大 21 亿对于日志表、操作记录表来说两三年就可能触顶。eladmin 的基础表主键是数据库自增的 bigint我把它的序列生成器理解为一个包工头每次分配号段时都找数据库要一批号码拿到本地慢慢用。用它迁移到分库分表时也能相对平滑因为 bigint 的取值范围给分布式 ID 留了余地不至于换主键策略时推倒重来。时间字段用 datetime 而不是 timestamp同时保留 create_time、update_time 两个标准字段。timestamp 的存储范围到 2038 年就会出问题而且受时区影响之前排查过一个数据展示错乱的问题最后定位到是 JDBC 连接串里没指定 serverTimezone配合 timestamp 的会话时区把时间偏移了 8 小时。改成 datetime 后所有时间记录都按北京时间存储再没出过这类问题。另外 create_by、update_by 这两个审计字段被很多人忽略我在接手别人的项目时经常查不清某条数据谁改的eladmin 这种设计至少能够通过 Coded 查到人。逻辑删除用 deleted 字段而不是物理 DELETE。这是 eladmin 的一个典型设计所有业务表几乎都有 deleted 字段0 未删除1 已删除。这样做的好处是数据可追溯误删能恢复坏处是每个查询都要带上 deleted 0 条件如果字段没进索引查询性能会打折扣。对于后台管理系统这种需要审计的场景我认为逻辑删除利大于弊。但注意日志表、临时表这类数据量巨大且没有追溯价值的表不要用逻辑删除否则表膨胀速度会让你怀疑人生。我第二个项目的 sys_log 就是逻辑删除上线半年后表里堆了一千多万行最后只能写脚本分批清理极其痛苦。2.3 唯一索引与业务约束的设计巧思eladmin 表结构里对唯一性约束的把控值得单独拿出来说。sys_user 表对 username 建了唯一索引sys_role 对 name 建了唯一索引sys_dept 对 name 建了唯一索引。这些唯一索引除了防止重复数据更重要的是给应用程序的幂等操作兜底。我在开发第一个项目时遇到一个真实场景运营后台新增用户前端连续点了两次提交按钮第一次请求成功第二次因为网络抖动在网关层重试了一次结果数据库里出现了两条一模一样的 username 记录。由于代码里没有显式校验 username 是否已存在这个问题一直到我排查用户登录混乱时才暴露。加上唯一索引后第二次插入直接被数据库拒掉应用层捕获 DuplicateKeyException 提示用户名已存在问题从根上解决了。提示唯一索引的使用要注意与逻辑删除字段的配合。如果业务允许用户删除后重新注册同样的用户名那么唯一索引就不能建在 (username) 上而应该建在 (username, deleted) 上。但这种写法在数据量大的时候索引存储空间会增加属于典型的空间换一致性方案。关于索引的设计eladmin 的基础表并不算激进但 MyBatis Plus 的自动填充和逻辑删除插件要求特定字段存在这也是设计的隐性约束。我在扩展业务表时遵循了同样的规则主键 id、create_by、create_time、update_by、update_time、deleted 六件套缺一不可业务字段一律小写下划线命名外键字段统一以 _id 结尾。团队新成员接手时只需要看一眼表结构就知道这个项目的主人是干什么的这就是好设计带来的纯粹效率。3. 核心表结构实操解读数据库优化必须建立在看得懂表的基础上3.1 sys_user 与关联表查询链路里最常见的性能瓶颈sys_user 表是几乎所有查询的起点。用户列表、角色分配、部门树、数据权限过滤全都绕不开它。说实话我一开始并没觉得这张表有什么特殊直到一次线上事故让我意识到它的重要性。当时运营同事反馈用户管理页面打开要 5 秒以上我直接定位到 sys_user 的列表查询。eladmin 默认的用户列表查询会关联 sys_dept查部门名称、sys_users_roles查角色、sys_role查角色名称一条 SQL 里做了三次 LEFT JOIN。问题出在 sys_users_roles 这张中间表上没有针对 role_id 建索引关联查询时它全表扫描而这张表在数据量上升后膨胀速度远超我的预期。排查过后我做的优化很简单ALTER TABLE sys_users_roles ADD INDEX idx_user_id (user_id); ALTER TABLE sys_users_roles ADD INDEX idx_role_id (role_id); ALTER TABLE sys_log ADD INDEX idx_create_time (create_time);加完这三个索引后用户列表页的响应时间直接降到了 800 毫秒以内。这件事给我的教训是eladmin 的中间表和外键字段默认没有全覆盖索引业务上线前必须按实际查询链路补充。另外sys_user 表的 username 字段虽然有唯一索引但如果登录时用手机号或邮箱作为账号就要额外建立对应的唯一索引。eladmin 默认只支持 username 登录我在业务里接入了手机号登录于是在 sys_user 上加了 phone 字段并建了 idx_phone 唯一索引。这样在登录入口通过手机号直接索引命中而不是先模糊查询全表再比对性能和安全防撞库都能兼顾。3.2 菜单权限表与数据权限理解 permission 标识的工作机制sys_menu 表在普通开发眼里是配菜单用的实际上它包含了权限标识符 permission 字段这才是整个权限系统的关键。举个例子sys_menu 里有一行记录name 字段叫用户新增type 是 2按钮类型permission 是 UserController.create。后端的 UserController.create 方法上加了 PreAuthorize(el.check(UserController.create)) 注解。当用户登录后系统会把这个用户的所有角色关联的菜单权限标识收集起来放到 Redis 里接口被访问时用注解里的标识去 Redis 查有没有匹配项有就放行没有就 403。这个机制要配合数据库优化来理解权限标识的查询非常频繁每个接口请求都会触发一次校验而这个校验的核心数据是从 Redis 读取的避免了频繁查询 sys_menu 表。也就是说数据库的设计虽然重要但真正承载高并发压力的是缓存层。我这里面的实操经验是新增菜单或按钮权限后如果用户刷新后权限没变化通常是因为 Redis 里旧数据没清理需要调用 eladmin 自带的清理缓存功能或重启服务。sys_menu 表里还有 pid 字段用于构建树形菜单。eladmin 在构建菜单树时通过 pid 把所有菜单一次性查出来在内存中组装父子关系而不是 SQL 里递归。这种设计对菜单这种量级几百条特别合适但对数据体量极大的场景比如几十万条一次性查询会有极限。我自己处理过一个过万条菜单数据的极端场景多租户商城后台后来改成了懒加载模式指定父节点 ID 查询子节点才解决了首屏加载问题。不过这种场景比较罕见一般团队不会遇到。3.3 日志表与定时任务表被低估的数据膨胀风险点很多人在做 eladmin 数据库优化时把精力全放在用户、角色、菜单这些业务表上却忽略了日志表和定时任务相关的表。这两类表的增长速度是最快的但又是最容易在评审时被遗漏的。sys_log 表记录所有操作日志包括登录日志、异常日志、业务操作日志。每次 Controller 请求都会落一条内容包含请求参数、IP、耗时、请求方法等单条数据虽然不大但量大得惊人。默认情况下 eladmin 没有日志自动清理机制只提供手动清理接口。我建议在数据库层面通过事件调度器或凌晨定时脚本定期归档和清理 90 天前的日志数据避免日志表拖累系统整体性能。sys_quartz_job 和 sys_quartz_log 是定时任务相关的表。sys_quartz_log 记录每次任务执行的历史如果任务频次高比如每分钟一次积累速度也很可观。我在实际业务里配置了一个每 30 秒执行一次的数据同步任务上线两周后这张表就积了十万条数据。后来我调整了策略任务日志只保留最近 200 条同步是否成功通过单独的业务告警来判断。注意日志表清理不是业务功能不要通过应用层循环 DELETE。最好是 MySQL 层面按天分表比如 sys_log_20250101或者定期把历史数据导入归档库。对大多数项目来说保留 90 天数据足够排查问题更久远的数据价值并不高。4. 数据库优化实战从索引调整到慢 SQL 治理的完整路径4.1 慢 SQL 排查工具与方法动手前先找到病灶对 eladmin 做数据库优化第一步绝对不是改代码而是把慢 SQL 找出来。MySQL 自带的慢查询日志可以这么开启-- 查看当前状态 SHOW VARIABLES LIKE slow_query_log; SHOW VARIABLES LIKE long_query_time; -- 开启慢查询日志生产环境建议只临时开启 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 2;把 long_query_time 设置为 2 秒执行时间超过 2 秒的 SQL 会被完整记录下来。慢查询日志文件位置可以通过 SHOW VARIABLES LIKE slow_query_log_file 查看。开启慢查询日志后我一般配合 MySQL Workbench 或直接分析日志文件。业界常用 pt-query-digest 这类工具来聚合分析但在小团队里不需要那么复杂直接用 tail 实时观察一段时间就能发现问题集中点。以我的经验eladmin 系统里最常见的慢 SQL 有三类多表关联查询缺少索引、分页查询 ORDER BY 字段无索引、大表的 COUNT(*) 统计。COUNT 的问题特别典型。列表页要显示共 xxx 条时eladmin 的分页接口会执行 SELECT COUNT(*) FROM xxx WHERE deleted 0。当一张表的数据量超过百万时这个 COUNT 可能比主查询还慢。我遇到过一次服务端接口超时定位到就是 COUNT 语句在 sys_log 表上执行了 4 秒多。优化的思路是业务允许时不要每次请求都刷新总数缓存总量并定时更新或者直接去掉总条数显示改成加载更多的无限滚动模式。4.2 索引优化策略从覆盖索引到最左匹配的实际案例eladmin 的表结构里主键索引都是聚簇索引二级索引的叶子节点存的是主键值。所以二级索引用得好不好直接影响回表次数和查询效率。举一个我在项目中实际处理过的例子。用户列表页面有一个筛选条件按部门查用户执行逻辑类似SELECT * FROM sys_user WHERE dept_id 100 AND deleted 0 ORDER BY create_time DESC LIMIT 10 OFFSET 0;mybatis 日志打印这条 SQL 后我通过 EXPLAIN 发现它走了全表扫描。原因很简单sys_user 表上 dept_id 没有索引。加上索引之后优化效果立竿见影ALTER TABLE sys_user ADD INDEX idx_dept_id (dept_id);但 DEADLINE 是如果业务查询经常带多个条件就要考虑联合索引。比如前端筛选经常是 dept_id status create_time 三个条件组合那么应该建立联合索引 idx_dept_status_time (dept_id, status, create_time)。这里遵循最左匹配原则把区分度高的字段放在最前面。dept_id 的区分度通常远高于 status比如 status 只有 0 和 1 两种值所以放在最左是合理的。我还能分享一个关于排序优化的细节。ORDER BY create_time DESC 这个排序字段如果不在索引里MySQL 就不得不使用 filesort数据量大的时候会显著拖慢查询。把排序字段放进联合索引例如 (dept_id, create_time)配合最左匹配规则就能同时覆盖 WHERE 和 ORDER BY避免额外排序。这里要小心别把索引建得太宽索引不是越多越好每个索引都会增加写入和存储成本。索引字段长度也要控制。对于中文字符串比如部门名称、角色名称如果必须加索引建议只对前缀建索引比如ALTER TABLE sys_dept ADD INDEX idx_name (name(20))。这样可以显著减少索引占用的空间因为 MySQL 对索引键值长度有限制InnoDB 单列索引最长 3072 字节而且太长的索引不仅存储成本高随机 IO 也会增加。4.3 缓存策略Redis 在 eladmin 数据库优化中的角色eladmin 的数据库优化不能只盯着 MySQL它内置的 Redis 缓存机制对数据库压力分担极其重要。我在做优化时反复强调一个观点每次请求直接打 MySQL 的系统注定会被流量压垮必须让 Redis 挡住重复的读请求。eladmin 的缓存策略主要有几个层面用户登录信息和权限标识缓存key 通常是token:xxxvalue 包含用户基本信息、角色编码、权限标识集合。这个缓存的生命周期和登录态一致配合 JWT 使用大大减少了登录状态校验对数据库的依赖。数据字典缓存sys_dict 表的数据都缓存到 Redis业务代码里调用 Cacheable 注解获取字典避免每次翻译字典都去查 MySQL。菜单树缓存用户登录后拉取自己可见的菜单树并缓存非敏感数据也会加缓存以减少 JDBC 压力。我在项目里踩过一个缓存相关的坑印象很深。当时改了 sys_dict 表里的某个字典项但在业务代码中老读到旧值。排查后发现是 Cacheable 注解默认的缓存 key 没有包含字典类型条件导致所有字典共享同一个缓存 key。修改后我把 key 设计成dict:${dictType}:${dictValue}这种粒度确保数据更新后能按类型精准失效。这个问题的本质是缓存粒度设计不合理和数据库设计一样设计得好能省很多事设计得差就天天出故障。在实践中我还发现eladmin 自带的消息通知模块和定时任务模块频繁地读写数据库这部分也要通过缓存优化。对于实时性要求不高的统计数据比如本周新增用户数我用 Cacheable 缓存十分钟数据库压力立刻下降了一个量级。4.4 连接池与 JVM 参数的配合调优除了 SQL 本身和缓存数据库连接池的配置也会直接影响性能。eladmin 默认使用的是 HikariCP 连接池Spring Boot 配置如下spring: datasource: url: jdbc:mysql://localhost:3306/eladmin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: root hikari: # 连接池最大连接数 maximum-pool-size: 20 # 连接池最小空闲连接数 minimum-idle: 5 # 连接在池中的最大存活时间 max-lifetime: 1800000 # 连接超时时间 connection-timeout: 30000maximum-pool-size 不是越大越好。MySQL 默认最大连接数是 151如果应用连接池设置 200数据库直接无法接受更多连接了。一般单实例 Spring Boot 应用我建议 maximum-pool-size 设置为 20-50并预留一些连接给其他管理工具使用。连接数要根据压测结果调整先设置 20压测观察响应时间和数据库 CPU 波动再逐步上调。core 线程数和 IO 密集度决定了最佳连接数没有统一答案但超额设置一定有害。max-lifetime 这个参数也要关注。MySQL 服务器默认 wait_timeout 是 8 小时HikariCP 的最大连接存活时间如果设置的比数据库的 wait_timeout 还长连接会在数据库中驻留非常久频繁占用 MySQL 并发连接资源。建议 max-lifetime 设置为 30 分钟这样连接在数据库主动断开前就会被连接池回收。JVM 参数同样影响数据库连接的使用效率。我在部署 eladmin 的 Java 服务时常用的 JVM 参数示例-Xms1024m -Xmx2048m -XX:UseG1GC -XX:MaxGCPauseMillis200堆内存设置的太小会导致频繁 Full GC进而导致应用请求响应变慢、数据库连接被长占。堆内存设的太大又可能引发内存溢出、系统资源不足。具体大小要根据部署机器的总内存和业务并发量来定。我第一次部署时给了 4G 堆内存结果当时主机只有 4G 内存加上操作系统和其他服务的内存占用系统直接 OOM。后来改成 2G 堆虽然没有更快但至少稳定。5. 常见问题排查与优化实录从真实故障中学到的经验5.1 用户列表查询越来越慢一个索引优化的完整复盘这个案子发生在第二个项目上线一周后运营同事反馈用户管理页面越来越卡从最初的 1 秒掉到了 4 秒。我拿到问题后的排查过程是这样的打开慢查询日志抓到了那条关联查询的慢 SQL耗时 3.8 秒。用 EXPLAIN 分析执行计划发现三张表的关联中sys_users_roles 表走了全表扫描rows 预估 80 多万sys_role 表走了全表扫描rows 预估 20 多万。查看 sys_users_roles 这张中间表的索引情况发现只有自增主键没有 user_id 和 role_id 的索引。在 sys_users_roles 表上补充 idx_user_id 和 idx_role_id 之后再次 EXPLAIN关联类型全部变成了 refrows 降到了个位数。这条 SQL 的执行时间从 3.8 秒降到了约 0.1 秒。这个案例里最典型的地方在于问题不是新增业务引入的而是数据量增长后基数变化暴露了设计的盲区。80 万条中间表数据对于 MySQL 来说不算大但没有索引时全表扫描依然会让响应慢到不可用。经验上线前的数据库评审一定要带上数据量增长十倍后可用性是否依然成立这样的思考角度。5.2 分页查询 offset 深翻页为什么 LIMIT 100000, 10 会卡eladmin 列表页做了分页用户可以从第 1 页翻到第 10000 页。MyBatis Plus 的分页插件最终生成的 SQL 是 LIMIT 100000, 10。当偏移量很大时MySQL 需要扫描 100010 行数据然后丢弃前 100000 行非常浪费。我在一个大数据量的查询业务上遇到过这种深分页问题一个 500 万行的业务表翻到第 5000 页时接口超时。解决方案是改成分页游标或者键集分页-- 普通深分页慢 SELECT * FROM biz_order WHERE deleted 0 ORDER BY id LIMIT 100000, 10; -- 键集分页快 SELECT * FROM biz_order WHERE deleted 0 AND id 100000 ORDER BY id LIMIT 10;键集分页利用主键索引的有序性直接跳过偏移扫描性能提升非常明显。代价是页面上不能显示跳转到第 N 页功能只能上一页下一页。我在业务需求允许的情况下都会改用这种方式。对 eladmin 自带的用户列表和日志列表如果数据量可控普通 LIMIT 分页问题不大但如果日志数据积累过多就建议改成键集分页。5.3 N1 查询问题关联查询为什么总是多查几次数据库eladmin 使用 MyBatis Plus 时如果开发者用循环查字典、循环查关联表的方式写业务就会出现经典的 N1 查询问题。比如查询 20 个用户的部门名称代码里循环调用了 20 次 deptService.findById加上用户本身的主查询总共 21 次数据库往返。这种问题在数据量少时察觉不到一旦列表条数增加数据库压力呈线性暴涨。排查方法很简单开启 MyBatis 的 SQL 日志观察一个列表接口打印了多少条 SELECT。如果明显大于预期比如列表 100 条数据打印了 100 条以上 SQL基本可以断定存在 N1。解决思路第一条关联数据量不大时一次性查出所有部门 ID用 WHERE id IN (...) 批量查询部门信息然后内存中组装。第二条利用 MyBatis Plus 的 QueryWrapper 直接写联表查询或者自定义 XML一次查询返回所需字段。第三条对字典类静态数据全部走 Redis 缓存避免每次翻译都打数据库。我在项目里用自定义 XML 写了一个用户列表查询通过 LEFT JOIN 一次性把用户名、角色名、部门名称全部查出N1 问题直接消失。5.4 缓存穿透与缓存失效Redis 加持后的数据一致性维护缓存虽好但用不好也会带来各种问题。这里说的不是极端攻击场景而是日常业务里很容易碰到的几种情况。第一个是缓存过期导致的瞬时压力。如果一个热点 key比如所有用户共享的权限配置在同一时刻过期一瞬间所有请求都打到 MySQL数据库可能扛不住。解决方案是给过期时间加一个随机偏移量避免缓存集体失效。eladmin 里字典缓存是典型的热点数据我把缓存过期时间设置为 1 到 3 小时之间的随机值效果很好。第二个是缓存与数据库的一致性。直接删缓存再更新数据库查询时再写缓存是最朴素的做法但存在空窗期。更稳妥的方式是更新数据库后延迟双删——先删缓存再更新数据库稍等几百毫秒再删一次缓存。eladmin 基于 Redis 的缓存实现里CacheEvict 就是删缓存操作一般不用手动配置延迟双删但如果你自己写了缓存读写逻辑就要注意这个问题。第三个容易出现的是缓存里放了大对象。比如把整个用户列表序列化成 JSON 存到 Redis一次性读取倒是快但数据更新时缓存失效和重建成本都很高。我的原则是缓存只存热点且变化不频繁的数据粒度要细能够按 key 精准失效。eladmin 本身的缓存粒度设计就比较好业务开发时不要破坏这个约定。5.5 数据库表结构变更脚本管理生产环境不敢乱动手eladmin 的数据库优化绕不开表结构变更。开发时可以直接改表生产环境就不一样。我踩过最疼的一个坑是在测试环境直接执行了 ALTER TABLE 修改字段类型当时看起来没问题上线时把开发库的表结构同步到了生产库结果字段长度变化导致线上部分历史数据被截断。从那以后我强制要求所有表结构变更必须走 SQL 脚本带版本号通过 Flyway 或 Liquibase 管理。对于 eladmin 项目推荐直接集成 Flywayspring: flyway: enabled: true baseline-on-migrate: true locations: classpath:db/migration在 db/migration 目录下放 V1__init.sql、V2__add_index.sql 这样的脚本服务启动时自动按版本顺序执行。这样数据库变更可追溯、可回滚配合备份、可多人协作不会出现谁改了表结构大家都不知道的情况。索引变更也建议走脚本。我自己有一条经验任何数据库变更先在测试环境用真实数据量的十分之一跑一遍确认没有锁表风险后再安排生产窗口执行。给大表加索引时InnoDB 的在线 DDL 虽然允许 DML 并发但依然会占用额外 IO要在低峰期做。6. 优化效果评估与后续演进的个人思考eladmin 的数据库优化做到什么程度算到位我不会拿监控面板上的数字当唯一指标更倾向用三个业务指标来评估列表页平均响应时间、登录接口 P99 耗时、数据库 CPU 使用率。拿我之前优化的项目来说优化前用户列表平均 3 秒优化后 500 毫秒以内数据库 CPU 使用率高峰期从 85% 降到了 30% 左右慢查询数量从每天几百条降到了个位数。这里最关键的不是把单个 SQL 调快而是建立一套持续优化的机制。我的做法是每月定期从慢查询日志里拉取 Top 10 SQL逐一复审执行计划发现问题立即处理。业务在变数据量在变以前的优化方案可能半年后就不适用了。后续如果业务量继续增长我优先会考虑的方向是分库分表。以 sys_log 为例按时间分区或者按月分表可以缓解单表数据膨胀带来的索引失效问题。其次就是读写分离把报表查询、统计查询引导到从库减轻主库负载。不过这些方向对团队运维能力的要求比较高MySQL 主从同步延迟、数据一致性、分布式事务都是新的课题。eladmin 这种规模的系统把单实例优化做到极致已经能覆盖绝大多数业务需求分库分表不是万金油盲目引入只会增加复杂度。最后聊一点我的个人心得数据库设计和优化的本质并不是掌握多少高级技巧而是对自己系统的数据流有清晰的认知。eladmin 给了我们一套设计良好的起点但能不能在业务增长的过程中持续保持顺滑就要靠每一位开发者对表结构、索引、缓存、SQL 执行计划的敏感度。没事多看看 EXPLAIN 的输出多留意慢查询日志比看再多理论文章都有用。