1. 项目概述
作为一名JavaWeb开发者,日志技术、多表查询和分页查询是我们日常开发中必须掌握的核心技能。今天我要分享的是在实际项目中如何高效实现这些功能,特别是如何通过动态SQL来优化分页查询性能。
记得我刚入行时,面对一个需要展示100万条数据的报表需求,直接使用简单的limit分页导致页面加载需要30多秒。后来通过优化SQL和引入缓存,最终将响应时间控制在1秒内。这段经历让我深刻认识到,掌握这些技术不仅仅是会用API那么简单,更需要理解背后的原理和优化思路。
2. 日志技术实战
2.1 日志框架选型
在Java生态中,我们常用的日志框架有Log4j2、Logback和SLF4J。我推荐使用Log4j2作为日志实现,主要有以下几个原因:
- 异步日志性能优异,比Logback快6-10倍
- 支持丰富的Appender(控制台、文件、数据库等)
- 灵活的Filter和Layout配置
<!-- Maven依赖配置 --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.1</version> </dependency>2.2 日志最佳实践
在实际项目中,我总结了以下日志使用经验:
日志级别使用规范:
- ERROR:系统错误,需要立即处理
- WARN:潜在问题,需要关注
- INFO:重要业务流程日志
- DEBUG:调试信息
- TRACE:最详细的跟踪信息
日志内容优化:
- 避免在日志中打印大对象
- 使用占位符{}代替字符串拼接
- 敏感信息脱敏处理
// 错误示例 logger.info("用户信息:" + user); // 正确示例 logger.info("用户登录,userId={}, userName={}", user.getId(), mask(user.getName()));提示:生产环境一定要配置日志文件滚动策略和自动清理,避免磁盘被日志文件占满。
3. 多表查询实现
3.1 多表关联方式
在MySQL中,我们常用的多表关联方式包括:
- 内连接(INNER JOIN):只返回两表中匹配的行
- 左连接(LEFT JOIN):返回左表所有行,右表不匹配则为NULL
- 右连接(RIGHT JOIN):返回右表所有行,左表不匹配则为NULL
- 全连接(FULL JOIN):返回两表所有行,不匹配则为NULL
-- 示例:查询部门及其员工信息 SELECT d.dept_name, e.emp_name, e.position FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id WHERE d.status = 1 ORDER BY d.dept_id, e.emp_id3.2 MyBatis多表查询
在MyBatis中处理多表查询,我推荐以下几种方式:
- 使用ResultMap进行结果映射
- 使用association和collection处理一对一、一对多关系
- 对于复杂查询,可以使用@Select注解直接编写SQL
<resultMap id="deptWithEmployees" type="Department"> <id property="deptId" column="dept_id"/> <result property="deptName" column="dept_name"/> <collection property="employees" ofType="Employee"> <id property="empId" column="emp_id"/> <result property="empName" column="emp_name"/> <result property="position" column="position"/> </collection> </resultMap> <select id="selectDeptWithEmployees" resultMap="deptWithEmployees"> SELECT d.dept_id, d.dept_name, e.emp_id, e.emp_name, e.position FROM department d LEFT JOIN employee e ON d.dept_id = e.dept_id WHERE d.status = 1 </select>4. 分页查询优化
4.1 基础分页实现
最简单的分页方式是使用LIMIT子句:
SELECT * FROM employee ORDER BY emp_id LIMIT 10 OFFSET 20; -- 每页10条,第3页但在数据量大的情况下(比如100万条记录),这种分页方式会有性能问题,因为MySQL需要先扫描前20条记录。
4.2 优化方案:键集分页
对于大数据量表,我推荐使用基于索引键的分页方式:
-- 第一页 SELECT * FROM employee ORDER BY emp_id LIMIT 10; -- 后续页(假设上一页最后一条记录的emp_id是100) SELECT * FROM employee WHERE emp_id > 100 ORDER BY emp_id LIMIT 10;这种方式的优点是:
- 不需要计算总页数
- 每次查询都走索引,性能稳定
- 适合无限滚动加载场景
4.3 MyBatis-Plus分页插件
MyBatis-Plus提供了方便的分页插件:
// 配置类 @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } // 使用示例 Page<Employee> page = new Page<>(1, 10); // 当前页,每页大小 Page<Employee> result = employeeMapper.selectPage(page, null); List<Employee> records = result.getRecords(); long total = result.getTotal();5. 动态SQL优化
5.1 MyBatis动态SQL
MyBatis提供了强大的动态SQL功能,可以避免拼接SQL字符串:
<select id="selectEmployees" resultType="Employee"> SELECT * FROM employee <where> <if test="deptId != null"> AND dept_id = #{deptId} </if> <if test="name != null and name != ''"> AND emp_name LIKE CONCAT('%', #{name}, '%') </if> <if test="position != null"> AND position = #{position} </if> </where> ORDER BY emp_id </select>5.2 分页查询优化实践
结合动态SQL和分页查询,我们可以实现高性能的复杂查询:
public PageResult<EmployeeVO> queryEmployeePage(EmployeeQuery query) { // 1. 构建分页参数 Page<Employee> page = new Page<>(query.getPageNum(), query.getPageSize()); // 2. 执行分页查询 LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(query.getDeptId() != null, Employee::getDeptId, query.getDeptId()) .like(StringUtils.isNotBlank(query.getName()), Employee::getEmpName, query.getName()) .eq(StringUtils.isNotBlank(query.getPosition()), Employee::getPosition, query.getPosition()) .orderByAsc(Employee::getEmpId); Page<Employee> employeePage = employeeMapper.selectPage(page, wrapper); // 3. 转换为VO并返回 List<EmployeeVO> voList = convertToVOList(employeePage.getRecords()); return new PageResult<>(voList, employeePage.getTotal()); }5.3 连表分页查询优化
对于需要连表的分页查询,需要注意以下几点:
- 确保分页操作在主表上进行
- 使用JOIN时注意表关联字段是否有索引
- 考虑使用子查询先分页再关联
-- 优化后的连表分页查询 SELECT e.*, d.dept_name FROM employee e JOIN department d ON e.dept_id = d.dept_id WHERE e.emp_id IN ( SELECT emp_id FROM employee WHERE dept_id = 1 ORDER BY emp_id LIMIT 10 OFFSET 20 )6. 常见问题与解决方案
6.1 分页查询性能问题
问题现象:当数据量达到百万级时,分页查询变慢,特别是翻到后面几页时。
解决方案:
- 使用基于索引键的分页(如WHERE id > last_id)
- 添加合适的索引
- 考虑使用缓存前几页数据
6.2 多表查询结果映射错误
问题现象:MyBatis返回的结果集中,关联对象属性为null。
排查步骤:
- 检查SQL语句是否正确关联了表
- 检查ResultMap配置是否正确
- 检查数据库字段和Java属性名是否匹配
6.3 动态SQL条件失效
问题现象:动态SQL中的条件没有生效。
常见原因:
- 参数值为null时,if条件判断错误
- 参数名与OGNL表达式不匹配
- 使用了错误的比较运算符
7. 性能优化建议
根据我的项目经验,以下优化措施可以显著提升查询性能:
索引优化:
- 为所有查询条件字段添加索引
- 为排序字段添加索引
- 考虑使用复合索引
SQL优化:
- 避免SELECT *,只查询需要的字段
- 使用EXPLAIN分析SQL执行计划
- 对于复杂查询,考虑拆分为多个简单查询
缓存策略:
- 使用Redis缓存热点数据
- 实现二级缓存减少数据库压力
- 考虑使用本地缓存提高响应速度
连接池配置:
- 合理设置连接池大小
- 监控连接池使用情况
- 使用HikariCP等高性能连接池
// HikariCP配置示例 @Bean public DataSource dataSource() { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/my_db"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); return new HikariDataSource(config); }在实际项目中,我发现很多性能问题都源于不合理的索引设计和过度复杂的SQL。建议在开发阶段就使用性能测试工具对关键查询进行压测,及早发现并解决问题。