☰
eladmin数据库优化实战:从表结构设计到慢SQL排查
2026/9/29 16:14:35 网站建设 项目流程

1. 我先拆一下eladmin的数据链路和表结构设计

接到一个eladmin项目,很多人的第一反应是“框架都写好了,我直接往里塞业务代码就行”,结果业务跑了一两个月,慢SQL一封接一封告警,数据库CPU飙到90%,这时候回过头来查问题,才发现表结构、索引、SQL写法全都有隐患。这篇就把我在几个eladmin项目里踩过的数据库设计坑和优化思路完整捋一遍,项目刚起步的朋友参考这个能少走很多弯路。

1.1 eladmin为什么会选MyBatis而不是JPA

eladmin的技术栈是Spring Boot + MyBatis + MySQL,很多人会问为什么不用Spring Data JPA。我的看法是:eladmin定位是后台管理系统,业务大多是列表查询、条件筛选、批量操作、多表关联,这类场景SQL的可控性比对象映射的便利性更重要。MyBatis的好处是SQL是显式写的,DBA和开发可以一起review每一句查询,索引怎么走、join怎么优化,都有明确的抓手;JPA虽然开发快,但一旦复杂查询出现性能问题,排查成本会高很多。

另外一点,eladmin的代码生成器也是基于MyBatis来做的,生成的mapper接口和xml是一对一对应的。对于二次开发来说,新加一个模块时,直接通过generator生成CRUD代码,再手工调整查询条件,整个链路非常成熟。这个选型在数据库设计层面传递的信号是:你要对SQL负责,所以数据库设计得更谨慎。

1.2 eladmin核心表拆解:用户、角色、菜单、部门

eladmin的表结构里,我最常看的是sys_user、sys_role、sys_menu、sys_dept这四类,所有权限和业务的基础都压在这上面。sys_user和sys_role是典型的多对多关系,eladmin用sys_users_roles中间表来解耦;sys_role和sys_menu同样用sys_roles_menus中间表关联。这个设计的直接好处是权限模型非常规整,新增一种角色时不需要动用户表结构,只加关联记录就行。

sys_dept是树形结构,eladmin用parent_id自关联来实现部门层级。这个设计在数据库层面很轻,但是查询子树时如果靠递归或者多次查询,性能会很难看。实际项目中我一般会同步维护一个ancestors字段,存储从根节点到当前节点的id路径,用like查询子树就快很多。这个属于对原设计的补充,部门数据量大时尤其值得做。

还有一点不能忽略:sys_user表里的create_by、create_time、update_by、update_time这些审计字段几乎每个表都有。这是eladmin的自动填充功能实现的,MyBatis的MetaObjectHandler会统一处理。做数据库设计时,新表也建议沿用这套字段规范,后续排查数据问题、做对账,都能省不少力气。

1.3 字段类型选择:看起来简单,后期返工最头疼的地方

数据库字段类型选不好,后面优化SQL的时候会特别被动。我在eladmin里见过最多的返工就是状态字段用varchar存数字、金额字段用float存、时间字段用字符串存。这里给一个经过几个项目验证的字段选择清单:

业务含义推荐类型原因
主键IDbigint unsigned自增或雪花ID都有足够空间,避免int溢出
状态/类型/标志位tinyint0和1就够了,节省空间,查询也快
金额/价格decimal(10,2)float/double有精度问题,账目对不上就是事故
创建时间/更新时间datetime时区问题少,范围大,便于直接展示
业务名称/标题varchar(128)预留扩展空间,又不至于索引页浪费
富文本/长内容text或json不建索引,避免覆盖索引失效的问题

另外一个反模式是“万能字段”,也就是一个大字段存JSON,把各种属性都塞进去。eladmin本身的业务建议是尽量字段化,因为JSON里的属性根本没法建索引,也没法参与关联查询。短期看方便,长期看就是给自己埋雷。

2. 数据库优化的前提:先理解eladmin的执行链路

2.1 Controller到SQL的调用链路不要空看

eladmin的请求链路是Controller -> Service -> Mapper接口 -> MyBatis XML里的SQL -> 数据库。很多人调接口慢,上来就查SQL,但真正的问题可能出在Service层的逻辑上——比如在循环里查数据库、查完一个查另外的、关联数据用代码拼接。我遇到过典型的N+1问题:列表接口返回100条用户数据,每条用户再去查一次角色和部门,数据库连接池直接被拖垮。

排查这种问题有个笨办法但很有效:打开MySQL通用日志或者开启MyBatis的SQL日志,看一次请求触发了多少条SQL。如果一次请求产生了几十上百条SQL,基本可以断定是N+1,优先去Service层把循环查询改成批量查询。用MyBatis的foreach批量查,一次IN查询可以替代几十次单条查询,接口响应时间往往能降一个数量级。

2.2 MyBatis代码生成器生成的SQL不能无脑用

eladmin继承了MyBatis Generator的能力,会自动生成基础的CRUD SQL。生成的selectByPrimaryKey和insert这类简单语句没问题,但生成的selectByExample有一个性能隐患:它会在XML里生成所有字段的查询列,用where拼接条件。如果表有几十个字段,查出来的行又很宽,走索引扫描还好,一旦走了全表扫描,IO开销会很夸张。

我的做法是:生成的XML只当CRUD底座,核心列表查询、统计报表查询全部手写SQL,表名和字段名都用反引号标好,查询列只select需要的字段。对于后台列表页,select *是大忌,因为表格往往只展示其中几列,宽带窄带差距在数据量大时非常明显。

还有一点:generator默认生成的mapper方法会覆盖同名列和同参方法,如果你在XML里手工加过自定义SQL,再重新生成时会冲突。所以我的习惯是自定义SQL放到扩展Mapper里,比如XxxMapperExt,生成器生成的XxxMapper轻易不动。这个习惯能避开大量版本管理冲突。

2.3 PageHelper分页原理和分页SQL的几个坑

eladmin的分页插件是PageHelper,它实现的是物理分页,原理是在执行你写的查询SQL之前,拦截器自动拼接limit和count查询。这也是为什么eladmin里分页几乎不用手写limit的原因。

但PageHelper有几个坑必须知道。第一个坑是嵌套查询时,如果先查了外层再查内层,分页拦截器可能拦截到不期望的SQL,导致count不准确。解决办法是把所有数据先查出来再封装,避免在PageHelper生效的范围内继续执行查询。第二个坑是offset过大的时候,数据库仍然需要扫描前面所有的行,比如limit 100000, 20,MySQL要扫过10万行再丢掉前10万行。数据量到了几十万之后,分页就会越来越慢。

应对深分页,我常用两个方案。第一种是“带条件翻页”,也就是记住上一页最后一条记录的ID,查询时用where id > ? limit 20,这种翻页方式走主键索引,不管翻到第几页性能都稳定。第二种是延迟关联,先用覆盖索引查出主键ID,再join原表拿完整数据,避免大偏移量的回表损耗。eladmin的列表页数据量大时,这两个方案是真正的救命稻草,只是改动比PageHelper默认用法要花点功夫。

3. 索引设计与慢SQL排查:用实际案例走一遍全流程

3.1 eladmin里慢SQL是怎么暴露出来的

很多项目的慢SQL是等到用户投诉才发现的,其实MySQL本身提供了现成的慢查询日志。开启方式很简单,在my.cnf或mysqld配置中设置slow_query_log = ON,long_query_time = 1,表示超过1秒的SQL就会记录到slow.log。对于开发环境可以设置为0.5,对线上业务,超过1秒的查询基本都要关注。

但有个细节容易忽略:MySQL默认的long_query_time单位是秒,配置成1意味着只记录超过1秒的,如果线上已经有很多几百毫秒的查询堆积,慢日志里反而看不到。所以我一般会把阈值设成0.5或0.3,先把问题暴露出来,再逐条评估。eladmin这种管理系统,列表页请求几十个接口,单接口200ms是正常的,如果某个接口到了800ms以上,就要看是不是SQL有问题。

另外搭配一个工具非常好用:mysqldumpslow。它可以对慢日志聚合统计,输出Top N条最耗时的SQL,直接定位到具体的SQL文本和频率,不用一条条手动捞。

3.2 一条慢SQL从定位到优化的完整过程

之前在一个eladmin的工单模块里遇到一个查询,功能是统计某个部门当月处理的工单数量和平均响应时长。单表数据量是80万行左右,原始SQL大致长这样:

SELECT dept_id, COUNT(*) AS total_cnt, AVG(handle_hours) AS avg_hours FROM work_order WHERE create_time BETWEEN '2025-01-01' AND '2025-01-31' GROUP BY dept_id ORDER BY total_cnt DESC;

这条SQL在测试环境只有几万行的时候跑得飞快,上线到生产后直接要2.3秒。用EXPLAIN一看,type是ALL,rows显示76万,Extra里还有Using temporary和Using filesort。两个问题很明显:create_time没有索引导致全表扫描;GROUP BY dept_id不是索引覆盖,临时表和文件排序都出来了。

优化分两步走。第一步加索引:

ALTER TABLE work_order ADD INDEX idx_create_time_dept (create_time, dept_id);

这里用联合索引而不是单列索引,因为查询要按时间段过滤,同时又要按部门分组,字段合在同一个索引里才能让索引的B+树同时起到过滤和排序的作用。加完索引后再EXPLAIN,type变成了range,rows从76万降到了几千行,Extra里也不再出现Using filesort了。

第二步是避免全字段select,把不需要的列去掉。这一步看起来不起眼,但在覆盖索引场景下,如果只查create_time和dept_id,查询可以直接在索引页里完成,完全不用回表。最终查询耗时从2.3秒降到了差不多120毫秒。同样的思路可以复制到eladmin其他报表场景,凡是“时间范围+分组聚合”的查询,联合索引一定要覆盖到过滤和分组的列。

3.3 联合索引的左前缀原则和冗余索引清理

eladmin自带的表和业务新增的表上,经常能看到一串索引:idx_a、idx_a_b、idx_a_b_c,这其实很浪费。MySQL的联合索引遵守左前缀原则,最左列如果被过滤条件使用,后面的列才能继续参与索引匹配。如果你的联合索引是(a, b, c),那么查询条件是a alone、a+b、a+b+c都会用到该索引;但如果是b alone或者b+c,索引就不会生效。

所以设计联合索引时应该遵守两个原则:一是区分度高的列放前面,比如用户ID、状态等,避免把性别这种区分度低的列放最左;二是已有的(a, b)索引能覆盖a单独查询时,就没必要再单独建一个a索引。清理冗余索引的方法是查information_schema.statistics,按表分组列出所有索引和索引列,再逐条判断功能是否有重叠。删索引的前提是要在压测环境先跑一遍,确认没有查询依赖它,再在业务低峰期执行。

4. 连接池、缓存与读写分离:eladmin上线后的三板斧

4.1 HikariCP连接池参数不要用默认值硬扛

eladmin的Spring Boot项目默认集成的是HikariCP,很多项目直接用默认参数就跑线上,这是有隐患的。默认的maximumPoolSize是10,对于一个小型管理后台可能够用,但只要业务量上来,10个连接是绝对不够的。这里给一个相对靠谱的计算逻辑:连接池大小 = 机器CPU核心数 × 2 + 有效磁盘数。如果部署环境是4核的机器,一般设到10到15就够用,设太大反而容易把数据库连接打满。

eladmin里配置HikariCP的核心参数大致是这样的:

spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 15 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1

minimum-idle是连接池保持的最小空闲连接数,maximum-pool-size是最大连接数。connection-timeout设30秒已经算长了,如果客户端能在更短时间拿到连接就不该等这么久,设太长容易让请求堆积。max-lifetime是连接最大存活时间,MySQL默认wait_timeout是8小时,把max-lifetime设成30分钟或者60分钟,可以让连接在MySQL断开前及时被池子回收,防止使用失效连接。

另外值得提醒的是,连接池参数的调整要结合慢SQL排查一起做,不要只调连接池大小。如果很多SQL超过1秒还没执行完,连接池设再大也只是把问题往后推,数据库CPU照样会打满。

4.2 Redis缓存加在哪些位置才是合理的

eladmin自身支持整合Redis,但很多人接入缓存后喜欢一股脑把所有业务查询都缓存起来,结果缓存击穿、数据不一致轮番上线。我接的eladmin项目里,Redis缓存一般只加在两类位置。

第一类是字典数据和配置数据。这些数据的特点是读多写少、变动频率低,比如性别字典、工单类型、系统参数。把这类数据缓存到Redis,用key加TTL的方式,不但能减少数据库压力,还能让列表页的筛选项加载快一大截。第二类是报表类的统计结果。比如首页的多维统计、月度趋势图,这些数据往往要跑好几条聚合SQL,计算成本高,而且对实时性要求不高,缓存5分钟甚至10分钟完全没问题。

缓存要非常小心的是用户相关的查询。用户的角色、部门、权限如果将UserDetails缓存,一旦后台改了用户角色,由于缓存TTL没到,用户拿到的权限还是旧的,这就是典型的权限下发延迟问题。所以用户权限不建议简单缓存,或者一定要确保修改角色时主动删除对应缓存。这类问题排查起来非常隐蔽,往往用户投诉“我改了角色怎么没生效”,一看缓存还在。

缓存穿透、击穿、雪崩在eladmin里也常见。穿透建议用布隆过滤器做前置过滤,或者把空值也缓存起来;击穿可以用互斥锁或逻辑过期;雪崩则要给缓存TTL加随机扰动,防止大批key同一时间集中失效。不太推荐依赖默认TTL,建议在代码里设置缓存时加上一个随机范围。

4.3 数据超过单库瓶颈时,再考虑读写分离

eladmin项目单库能撑的数据量其实不小,在数据量低于几百万、QPS低于几千时,优先考虑索引优化和SQL优化,完全不需要上读写分离。等到主库写入压力高、报表查询又频繁占用大量IO时,再考虑MySQL主从复制。

读写分离的基本思路是主库负责写,从库负责读,eladmin的DataSource配置需要改造成动态数据源。这里要提醒的是,主从复制是有延迟的,刚写入的数据立即去从库查询可能查不到。业务上如果刚创建完记录马上要展示,这种强一致场景必须走主库。eladmin的Service里可以通过@Transactional(readOnly = true)标记查询方法,强制走从库,写操作方法则默认走主库,但在写后立即查的场景要手动指定走主库。

另外,从库不是越多越好。每加一个从库,主库要多一份binlog推送开销,从库数量在个位数以内是合理的。而且从库的硬件配置不能太低,很多公司把从库当成便宜的机器来用,结果从库CPU先被打满,查询全卡在从库上,最后还不如单库省心。

做读写分离前,先用慢日志统计一下当前SQL的读写比例。如果写占比低于20%,读占比超过80%,读写分离的价值比较明显。如果读写比例接近,光靠读写分离解决不了根本问题,还得从业务缓存和冷热数据拆分入手。

5. 冷热数据拆分和分库分表的决策时机

5.1 冷热分离比分库分表来得更早

很多团队一聊数据库优化就想到分库分表,其实在数据量没到亿级之前,冷热数据分离往往更实际,成本也更低。以eladmin的日志表为例,登录日志、操作日志、异常日志增长速度非常快,一个月可能新增上百万条。这些日志数据的特点是:最近几天的查询频率高,超过三个月的几乎没人看。

我的做法是给日志类大表增加月份分区,按create_time做range分区。比如:

ALTER TABLE sys_log PARTITION BY RANGE (YEAR(create_time) * 100 + MONTH(create_time)) ( PARTITION p202501 VALUES LESS THAN (202502), PARTITION p202502 VALUES LESS THAN (202503), PARTITION p202503 VALUES LESS THAN (202504), PARTITION p_max VALUES LESS THAN MAXVALUE );

这样查询三个月以内的日志,直接根据分区裁剪,不需要全表扫描。归档时把三个月前的分区detach出来,复制到历史库,线上库的体积能保持在一个稳定的水平。这个操作对eladmin这种自带日志功能的框架来说最匹配,不用改代码,只要在数据库层把表分好区。

分区表在业务逻辑上还是同一张表,对开发无感,但对DBA来说清理历史数据就很方便了。相比之下,分库分表要改动数据访问层,还得引入中间件或者使用分库分表框架,复杂度是数量级提升的。

5.2 分库分表不是你想象的那样非做不可

我见过很多项目,数据量才几百万,就开始规划分库分表,结果是系统复杂度上去了,性能并没有本质提升。分库分表真正要解决的痛点是单机写入瓶颈、超大单表导致的索引深度过高、以及查询并发超过单库承载能力。这些在eladmin的业务形态里,至少要千万级数据加上一定的并发才会遇到。

如果确实需要分库分表,建议先按业务垂直拆分,比如用户库、订单库、日志库分开,而不是上来就水平拆分用户表。垂直拆分成本低,对应用层只是数据源切换的问题。水平拆分要重点考虑分片键的选择,比如按user_id哈希取模,确定用户维度之后,所有查询都要带上分片键,否则就要遍历所有分片,这个在初期设计时就要想清楚。

另外一个需要冷静的点:eladmin管理后台的统计报表类查询,往往需要跨多个分片做聚合,这种查询在分库分表下极其痛苦。如果报表实时性要求高,建议引入离线分析库或走数据中台,否则就是在给自己挖坑。数据量没有大到单库扛不住,就不要动分库分表,这是很实在的劝告。

6. 这些优化在eladmin里的落地顺序和心态

从事务数据表的设计开始,每一步都要稳一点,先会用工具,再理解原理,再去做调优。eladmin这个系统本身的代码结构比较清晰,给了我们一个很好的试验场。建议按照先拆表结构,再查慢SQL,再调索引,再看缓存,最后再考虑扩展架构的顺序来走。不要上来就改数据源切换、改分库分表,那不是优化,那是重构,风险完全不一样。

把基础工作做好之后,这套方法论不仅适用于eladmin,迁移到任何Spring Boot + MyBatis的技术栈都能直接用。数据库优化靠的不是奇技淫巧,而是对执行计划、索引结构、业务查询模式这三件事的不断打磨。踩过的坑多了,自然就会形成一种条件反射:看到一条SQL,大概能猜到它的性能瓶颈在哪里。这种能力,比任何框架技巧都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询