Mybatis学习笔记(八)
性能优化与监控
简要描述:MyBatis-Plus的性能优化涉及SQL优化、缓存策略、连接池配置、监控诊断等多个方面,通过合理的配置和使用可以显著提升应用性能。
核心概念:
- SQL性能分析:分析SQL执行效率
- 慢查询优化:识别和优化慢查询
- 缓存策略:合理使用缓存提升性能
- 连接池优化:优化数据库连接池配置
- 监控诊断:实时监控和问题诊断
SQL性能分析
简要描述:通过各种工具和插件分析SQL执行性能,识别性能瓶颈。
核心概念:
- 执行计划分析:分析SQL执行计划
- 性能监控插件:MyBatis-Plus性能监控插件
- SQL统计:SQL执行统计信息
- 性能指标:关键性能指标监控
性能监控插件配置:
// 性能分析插件
@Configuration
public class PerformanceConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// SQL性能规范插件interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());// 分页插件PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInnerInterceptor.setMaxLimit(1000L);interceptor.addInnerInterceptor(paginationInnerInterceptor);return interceptor;}@Bean@Profile("dev")public PerformanceInterceptor performanceInterceptor() {PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();// 设置SQL执行最大时长,超过自动停止运行,有助于发现问题performanceInterceptor.setMaxTime(1000);// 设置SQL格式化,默认falseperformanceInterceptor.setFormat(true);return performanceInterceptor;}
}// 自定义性能监控插件
@Component
public class CustomPerformanceInterceptor implements Interceptor {private static final Logger logger = LoggerFactory.getLogger(CustomPerformanceInterceptor.class);@Overridepublic Object intercept(Invocation invocation) throws Throwable {long startTime = System.currentTimeMillis();try {Object result = invocation.proceed();long endTime = System.currentTimeMillis();long executeTime = endTime - startTime;// 记录执行时间if (executeTime > 100) { // 超过100ms的SQL记录警告MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];logger.warn("慢SQL检测 - 执行时间: {}ms, SQL ID: {}, 参数: {}", executeTime, mappedStatement.getId(), parameter);}return result;} catch (Exception e) {long endTime = System.currentTimeMillis();long executeTime = endTime - startTime;logger.error("SQL执行异常 - 执行时间: {}ms, 异常信息: {}", executeTime, e.getMessage());throw e;}}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// 设置属性}
}
执行计划分析工具:
// 执行计划分析工具
@Component
public class ExecutionPlanAnalyzer {@Autowiredprivate SqlSessionFactory sqlSessionFactory;public List<ExecutionPlan> analyzeExecutionPlan(String sql, Object... params) {try (SqlSession sqlSession = sqlSessionFactory.openSession()) {// 执行EXPLAIN分析String explainSql = "EXPLAIN " + sql;List<Map<String, Object>> results = sqlSession.selectList("analyzeExecutionPlan", Map.of("sql", explainSql, "params", params));return results.stream().map(this::convertToExecutionPlan).collect(Collectors.toList());}}private ExecutionPlan convertToExecutionPlan(Map<String, Object> result) {ExecutionPlan plan = new ExecutionPlan();plan.setId((Integer) result.get("id"));plan.setSelectType((String) result.get("select_type"));plan.setTable((String) result.get("table"));plan.setType((String) result.get("type"));plan.setPossibleKeys((String) result.get("possible_keys"));plan.setKey((String) result.get("key"));plan.setKeyLen((String) result.get("key_len"));plan.setRef((String) result.get("ref"));plan.setRows((Long) result.get("rows"));plan.setExtra((String) result.get("Extra"));return plan;}public boolean hasPerformanceIssues(List<ExecutionPlan> plans) {for (ExecutionPlan plan : plans) {// 检查是否有性能问题if ("ALL".equals(plan.getType()) || // 全表扫描plan.getRows() > 10000 || // 扫描行数过多plan.getExtra().contains("Using filesort") || // 文件排序plan.getExtra().contains("Using temporary")) { // 使用临时表return true;}}return false;}
}
慢查询优化
简要描述:识别、分析和优化慢查询,提升数据库查询性能。
核心概念:
- 慢查询日志:记录执行时间超过阈值的SQL
- 索引优化:合理创建和使用索引
- 查询重写:优化SQL语句结构
- 分页优化:优化大数据量分页查询
慢查询监控配置:
# application.yml
spring:datasource:druid:# 慢SQL记录filter:stat:enabled: trueslow-sql-millis: 1000log-slow-sql: true# 监控配置stat-view-servlet:enabled: trueurl-pattern: /druid/*reset-enable: falseweb-stat-filter:enabled: trueurl-pattern: /*exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"# MyBatis-Plus慢查询配置
mybatis-plus:configuration:# 开启SQL日志log-impl: org.apache.ibatis.logging.slf4j.Slf4jImplglobal-config:# 性能分析插件enable-sql-runner: truelogging:level:# 开启SQL日志com.example.mapper: debug# Druid慢SQL日志druid.sql.Statement: debug
慢查询优化策略:
// 慢查询优化服务
@Service
public class SlowQueryOptimizationService {@Autowiredprivate UserMapper userMapper;// 优化前:全表扫描public List<User> findUsersByNameBad(String name) {QueryWrapper<User> wrapper = new QueryWrapper<>();wrapper.like("username", "%" + name + "%"); // 前缀模糊查询,无法使用索引return userMapper.selectList(wrapper);}// 优化后:使用索引public List<User> findUsersByNameGood(String name) {QueryWrapper<User> wrapper = new QueryWrapper<>();wrapper.likeRight("username", name); // 右模糊查询,可以使用索引return userMapper.selectList(wrapper);}// 优化前:N+1查询问题public List<UserWithRoles> findUsersWithRolesBad() {List<User> users = userMapper.selectList(null);return users.stream().map(user -> {List<Role> roles = roleMapper.selectByUserId(user.getId()); // N+1查询return new UserWithRoles(user, roles);}).collect(Collectors.toList());}// 优化后:批量查询public List<UserWithRoles> findUsersWithRolesGood() {List<User> users = userMapper.selectList(null);if (users.isEmpty()) {return Collections.emptyList();}List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());// 批量查询角色List<UserRole> userRoles = userRoleMapper.selectList(new QueryWrapper<UserRole>().in("user_id", userIds));// 构建用户角色映射Map<Long, List<Role>> userRoleMap = userRoles.stream().collect(Collectors.groupingBy(UserRole::getUserId,Collectors.mapping(ur -> roleMapper.selectById(ur.getRoleId()),Collectors.toList())));return users.stream().map(user -> new UserWithRoles(user, userRoleMap.getOrDefault(user.getId(), Collections.emptyList()))).collect(Collectors.toList());}// 分页查询优化public IPage<User> findUsersWithPagination(int current, int size, String keyword) {Page<User> page = new Page<>(current, size);QueryWrapper<User> wrapper = new QueryWrapper<>();if (StringUtils.hasText(keyword)) {wrapper.and(w -> w.like("username", keyword).or().like("email", keyword));}// 优化:只查询必要字段wrapper.select("id", "username", "email", "create_time");return userMapper.selectPage(page, wrapper);}// 大数据量分页优化(游标分页)public List<User> findUsersWithCursorPagination(Long lastId, int size) {QueryWrapper<User> wrapper = new QueryWrapper<>();if (lastId != null) {wrapper.gt("id", lastId);}wrapper.orderByAsc("id");wrapper.last("LIMIT " + size);return userMapper.selectList(wrapper);}
}
索引优化建议:
-- 创建合适的索引
-- 1. 单列索引
CREATE INDEX idx_user_username ON user(username);
CREATE INDEX idx_user_email ON user(email);
CREATE INDEX idx_user_create_time ON user(create_time);-- 2. 复合索引(注意字段顺序)
CREATE INDEX idx_user_status_create_time ON user(status, create_time);
CREATE INDEX idx_user_dept_status ON user(dept_id, status);-- 3. 覆盖索引(包含查询所需的所有字段)
CREATE INDEX idx_user_cover ON user(status, username, email, create_time);-- 4. 前缀索引(对于长字符串字段)
CREATE INDEX idx_user_description ON user(description(50));-- 5. 函数索引(MySQL 8.0+)
CREATE INDEX idx_user_upper_username ON user((UPPER(username)));
缓存优化策略
简要描述:合理使用MyBatis一级缓存、二级缓存以及外部缓存系统,提升查询性能。
核心概念:
- 一级缓存:SqlSession级别的缓存
- 二级缓存:Mapper级别的缓存
- 外部缓存:Redis等外部缓存系统
- 缓存策略:缓存的使用策略和失效机制
MyBatis缓存配置:
# application.yml
mybatis-plus:configuration:# 开启二级缓存cache-enabled: true# 本地缓存作用域local-cache-scope: session# 懒加载配置lazy-loading-enabled: trueaggressive-lazy-loading: false
Redis缓存集成:
// Redis缓存配置
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 设置序列化器Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);serializer.setObjectMapper(mapper);template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())).disableCachingNullValues();return RedisCacheManager.builder(factory).cacheDefaults(config).build();}
}
Spring Cache注解使用:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 缓存查询结果@Cacheable(value = "users", key = "#id")public User findById(Long id) {return userMapper.selectById(id);}// 缓存查询结果(条件缓存)@Cacheable(value = "users", key = "#username", condition = "#username.length() > 3")public User findByUsername(String username) {QueryWrapper<User> wrapper = new QueryWrapper<>();wrapper.eq("username", username);return userMapper.selectOne(wrapper);}// 更新时清除缓存@CacheEvict(value = "users", key = "#user.id")public void updateUser(User user) {userMapper.updateById(user);}// 删除时清除缓存@CacheEvict(value = "users", key = "#id")public void deleteUser(Long id) {userMapper.deleteById(id);}// 清除所有缓存@CacheEvict(value = "users", allEntries = true)public void clearAllCache() {// 清除所有用户缓存}// 更新缓存@CachePut(value = "users", key = "#user.id")public User saveUser(User user) {userMapper.insert(user);return user;}
}
连接池优化
简要描述:优化数据库连接池配置,提升数据库连接效率和应用性能。
核心概念:
- 连接池大小:合理设置连接池大小
- 连接超时:设置合适的连接超时时间
- 连接验证:连接有效性验证
- 连接监控:连接池状态监控
Druid连接池优化配置:
# application.yml
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedruid:# 初始连接数initial-size: 10# 最小空闲连接数min-idle: 10# 最大活跃连接数max-active: 100# 获取连接等待超时时间max-wait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒time-between-eviction-runs-millis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒min-evictable-idle-time-millis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒max-evictable-idle-time-millis: 900000# 用来检测连接是否有效的sql,要求是一个查询语句validation-query: SELECT 1# 建议配置为true,不影响性能,并且保证安全性test-while-idle: true# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能test-on-borrow: false# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能test-on-return: false# 是否缓存preparedStatement,也就是PSCachepool-prepared-statements: true# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为truemax-pool-prepared-statement-per-connection-size: 20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙filters: stat,wall,slf4j# 通过connectProperties属性来打开mergeSql功能;慢SQL记录connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000# 合并多个DruidDataSource的监控数据use-global-data-source-stat: true# 配置web监控web-stat-filter:enabled: trueurl-pattern: /*exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"session-stat-enable: falsesession-stat-max-count: 1000principal-session-name: adminprincipal-cookie-name: adminprofile-enable: true# 配置监控页面stat-view-servlet:enabled: trueurl-pattern: /druid/*# IP白名单(没有配置或者为空,则允许所有访问)allow: 127.0.0.1,192.168.163.1# IP黑名单 (存在共同时,deny优先于allow)deny: 192.168.1.73# 禁用HTML页面上的"Reset All"功能reset-enable: false# 登录名login-username: admin# 登录密码login-password: 123456
HikariCP连接池优化配置:
# application.yml
spring:datasource:type: com.zaxxer.hikari.HikariDataSourcehikari:# 连接池名称pool-name: HikariCP# 最小空闲连接数minimum-idle: 10# 最大连接池大小maximum-pool-size: 100# 自动提交auto-commit: true# 空闲连接存活最大时间,默认600000(10分钟)idle-timeout: 600000# 连接池最大生命周期,0表示无限生命周期,默认1800000即30分钟max-lifetime: 1800000# 连接超时时间,默认30000即30秒connection-timeout: 30000# 测试连接是否可用的查询语句connection-test-query: SELECT 1# 连接初始化SQLconnection-init-sql: SET NAMES utf8mb4# 数据库连接超时时间,默认30秒,即30000validation-timeout: 5000# 空闲连接检测周期,默认30000毫秒keepalive-time: 30000# 是否允许连接泄露检测leak-detection-threshold: 60000
连接池监控:
// 连接池监控服务
@Service
public class DataSourceMonitorService {@Autowiredprivate DataSource dataSource;public DataSourceStats getDataSourceStats() {if (dataSource instanceof DruidDataSource) {return getDruidStats((DruidDataSource) dataSource);} else if (dataSource instanceof HikariDataSource) {return getHikariStats((HikariDataSource) dataSource);}return new DataSourceStats();}private DataSourceStats getDruidStats(DruidDataSource druidDataSource) {DataSourceStats stats = new DataSourceStats();stats.setActiveCount(druidDataSource.getActiveCount());stats.setPoolingCount(druidDataSource.getPoolingCount());stats.setMaxActive(druidDataSource.getMaxActive());stats.setCreateCount(druidDataSource.getCreateCount());stats.setDestroyCount(druidDataSource.getDestroyCount());stats.setConnectCount(druidDataSource.getConnectCount());stats.setCloseCount(druidDataSource.getCloseCount());return stats;}private DataSourceStats getHikariStats(HikariDataSource hikariDataSource) {DataSourceStats stats = new DataSourceStats();HikariPoolMXBean poolBean = hikariDataSource.getHikariPoolMXBean();stats.setActiveCount(poolBean.getActiveConnections());stats.setPoolingCount(poolBean.getIdleConnections());stats.setMaxActive(hikariDataSource.getMaximumPoolSize());stats.setTotalConnections(poolBean.getTotalConnections());return stats;}@Scheduled(fixedRate = 30000) // 每30秒监控一次public void monitorDataSource() {DataSourceStats stats = getDataSourceStats();// 记录监控日志logger.info("数据源监控 - 活跃连接: {}, 空闲连接: {}, 最大连接: {}", stats.getActiveCount(), stats.getPoolingCount(), stats.getMaxActive());// 检查连接池健康状况if (stats.getActiveCount() > stats.getMaxActive() * 0.8) {logger.warn("连接池使用率过高: {}%", (double) stats.getActiveCount() / stats.getMaxActive() * 100);}}
}
监控与诊断
简要描述:通过各种监控工具和诊断手段,实时监控MyBatis-Plus应用的性能状况。
核心概念:
- 性能指标监控:关键性能指标的实时监控
- 健康检查:应用健康状况检查
- 链路追踪:分布式链路追踪
- 告警机制:异常情况告警
Actuator监控配置:
# application.yml
management:endpoints:web:exposure:include: "*"endpoint:health:show-details: alwaysmetrics:enabled: truemetrics:export:prometheus:enabled: truedistribution:percentiles-histogram:http.server.requests: truepercentiles:http.server.requests: 0.5, 0.9, 0.95, 0.99
自定义健康检查:
// 数据库健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@Overridepublic Health health() {try (SqlSession sqlSession = sqlSessionFactory.openSession()) {// 执行简单查询测试数据库连接sqlSession.selectOne("SELECT 1");return Health.up().withDetail("database", "Available").withDetail("validationQuery", "SELECT 1").build();} catch (Exception e) {return Health.down().withDetail("database", "Unavailable").withDetail("error", e.getMessage()).build();}}
}// MyBatis-Plus健康检查
@Component
public class MybatisPlusHealthIndicator implements HealthIndicator {@Autowiredprivate UserMapper userMapper;@Overridepublic Health health() {try {// 测试基本CRUD操作long count = userMapper.selectCount(null);return Health.up().withDetail("mybatis-plus", "Available").withDetail("userCount", count).build();} catch (Exception e) {return Health.down().withDetail("mybatis-plus", "Unavailable").withDetail("error", e.getMessage()).build();}}
}
性能指标收集:
// 自定义性能指标
@Component
public class MybatisPlusMetrics {private final MeterRegistry meterRegistry;private final Counter sqlExecutionCounter;private final Timer sqlExecutionTimer;private final AtomicLong slowQueryCount = new AtomicLong(0);public MybatisPlusMetrics(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;this.sqlExecutionCounter = Counter.builder("mybatis.sql.executions").description("Total SQL executions").register(meterRegistry);this.sqlExecutionTimer = Timer.builder("mybatis.sql.duration").description("SQL execution duration").register(meterRegistry);Gauge.builder("mybatis.sql.slow.count").description("Number of slow queries").register(meterRegistry, this, MybatisPlusMetrics::getSlowQueryCount);}public void recordSqlExecution(String sqlId, long duration) {sqlExecutionCounter.increment(Tags.of("sql.id", sqlId,"status", "success"));sqlExecutionTimer.record(duration, TimeUnit.MILLISECONDS,Tags.of("sql.id", sqlId));}public void recordSqlError(String sqlId, String errorType) {sqlExecutionCounter.increment(Tags.of("sql.id", sqlId,"status", "error","error.type", errorType));}private double getSlowQueryCount() {return slowQueryCount.get();}public void incrementSlowQueryCount() {slowQueryCount.incrementAndGet();}
}