mybatis调优

0 阅读4分钟

以下是一些 MyBatis 性能调优的 具体场景和代码示例,结合 PostgreSQL 数据库的实战优化策略:


1. SQL 优化:避免 SELECT * 与强制索引

问题场景

假设 users 表有 100 万数据,查询时未走索引导致全表扫描。

优化方案

<!-- 优化前(全表扫描) -->
<select id="findByEmail" resultType="User">
  SELECT * FROM users WHERE email = #{email}
</select><!-- 优化后(强制索引) -->
<select id="findByEmail" resultType="User">
  SELECT id, name, email /* 明确指定字段 */
  FROM users 
  WHERE email = #{email}
  <!-- PostgreSQL 强制使用索引(需索引存在) -->
  <if test="_databaseId == 'postgresql'">
    USING INDEX idx_users_email
  </if>
</select>

关键步骤

  1. 创建索引:

    CREATE INDEX idx_users_email ON users(email);
    
  2. 使用 EXPLAIN 分析 SQL 执行计划:

    EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@example.com';
    

2. 批量插入优化:Batch 模式

问题场景

插入 10 万条数据时,逐条插入耗时过长。

优化方案

public void batchInsert(List<User> users) {
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        UserMapper mapper = session.getMapper(UserMapper.class);
        for (User user : users) {
            mapper.insert(user);
        }
        session.commit(); // 一次性提交
    }
}
​
// Mapper 接口
@Insert("INSERT INTO users (name, email) VALUES (#{name}, #{email})")
void insert(User user);

效果对比

  • 逐条插入:10 万条数据约 60 秒
  • Batch 模式:10 万条数据约 3 秒(提升 20 倍)

3. 二级缓存:跨 Session 共享缓存

问题场景

频繁查询相同配置数据(如省份列表),每次访问数据库。

优化方案

<!-- 在 Mapper XML 中启用二级缓存 -->
<mapper namespace="com.example.mapper.ProvinceMapper">
    <cache eviction="LRU" flushInterval="60000" size="1024"/>
    
    <select id="findAll" resultType="Province" useCache="true">
        SELECT * FROM provinces
    </select>
</mapper>

注意事项

  • 更新操作自动清空缓存:

    <update id="updateProvince" flushCache="true">
        UPDATE provinces SET name = #{name} WHERE id = #{id}
    </update>
    
  • 分布式环境下需改用 Redis 等集中式缓存。


4. 结果集映射优化:禁用自动映射

问题场景

复杂联表查询返回 100 列,自动映射字段耗时。

优化方案

<select id="findUserWithDetails" resultMap="UserDetailMap">
    SELECT 
        u.id, u.name, 
        a.street, a.city 
    FROM users u 
    LEFT JOIN addresses a ON u.id = a.user_id
</select><!-- 手动定义映射(避免反射开销) -->
<resultMap id="UserDetailMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="address" javaType="Address">
        <result property="street" column="street"/>
        <result property="city" column="city"/>
    </association>
</resultMap>

性能对比

  • 自动映射:1000 次查询耗时 1200ms
  • 手动映射:1000 次查询耗时 850ms

5. 分页优化:避免 OFFSET 深度翻页

问题场景

SELECT * FROM users OFFSET 100000 LIMIT 10 效率低下。

优化方案

<!-- 使用游标分页(基于索引列) -->
<select id="findUsersAfterId" resultType="User">
    SELECT * FROM users 
    WHERE id > #{lastId} 
    ORDER BY id ASC 
    LIMIT #{pageSize}
</select>

Java 调用逻辑

public List<User> paginateUsers(Long lastId, int pageSize) {
    return userMapper.findUsersAfterId(lastId, pageSize);
}

性能对比

  • 传统分页(OFFSET 100000):200ms
  • 游标分页:2ms

6. 动态 SQL 优化:避免过度嵌套

问题场景

多层 <if><choose> 导致 SQL 解析耗时。

优化方案

<!-- 优化前(多层嵌套) -->
<select id="searchUsers" resultType="User">
    SELECT * FROM users 
    <where>
        <if test="name != null">
            AND name LIKE #{name}
        </if>
        <choose>
            <when test="status != null">
                AND status = #{status}
            </when>
            <otherwise>
                AND status = 1
            </otherwise>
        </choose>
    </where>
</select>

<!-- 优化后(精简条件) -->
<select id="searchUsers" resultType="User">
    SELECT * FROM users 
    WHERE 
        <!-- 使用 COALESCE 简化逻辑 -->
        name = COALESCE(#{name}, name) AND
        status = COALESCE(#{status}, 1)
</select>

注意COALESCE 需结合数据库特性,此处为 PostgreSQL 语法。


7. 连接池优化:HikariCP 参数调优

application.yml 配置

spring:
  datasource:
    hikari:
      maximum-pool-size: 20          # 根据 CPU 核心数调整(建议:CPU * 2 + 1)
      minimum-idle: 5               # 避免连接突发创建
      connection-timeout: 3000      # 3秒超时(快速失败)
      max-lifetime: 1800000         # 30分钟(避免数据库防火墙断开)
      leak-detection-threshold: 5000 # 连接泄露检测(5秒)

监控指标

  • 活跃连接数(active)应小于 maximum-pool-size
  • 空闲连接数(idle)保持在 minimum-idle 附近

8. 类型处理器优化:JSONB 高效解析

场景

PostgreSQL 的 JSONB 字段频繁序列化/反序列化。

优化方案

// 自定义类型处理器
@MappedTypes(MyData.class)
@MappedJdbcTypes(JdbcType.OTHER)
public class JsonbTypeHandler extends BaseTypeHandler<MyData> {
    private static final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, MyData parameter, JdbcType jdbcType) {
        ps.setObject(i, mapper.valueToTree(parameter), Types.OTHER);
    }

    @Override
    public MyData getNullableResult(ResultSet rs, String columnName) {
        return parse(rs.getString(columnName));
    }

    private MyData parse(String json) {
        try {
            return mapper.readValue(json, MyData.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

注册处理器

@Configuration
public class MyBatisConfig {
    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> {
            configuration.getTypeHandlerRegistry().register(JsonbTypeHandler.class);
        };
    }
}

9. 日志监控:定位慢查询

启用 MyBatis 慢查询日志

<!-- mybatis-config.xml -->
<settings>
    <setting name="defaultStatementTimeout" value="30"/> <!-- SQL 超时时间 -->
</settings>

日志输出

// 使用 p6spy 记录真实 SQL(显示参数和耗时)
jdbc:p6spy:
  driver-class-name: com.p6spy.engine.spy.P6SpyDriver
  url: jdbc:p6spy:postgresql://localhost:5432/mydb

日志格式

# spy.properties
appender=com.p6spy.engine.spy.appender.Slf4JLogger
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(executionTime)ms | %(category) | %(sql)

10. 动态数据源切换:读写分离

场景

主库写入压力大,需分流查询到从库。

优化方案

// 自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

// AOP 切面
@Aspect
@Component
public class DataSourceAspect {
    @Around("@annotation(readOnly)")
    public Object switchDataSource(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
        String oldKey = DynamicDataSourceHolder.getDataSourceKey();
        DynamicDataSourceHolder.setDataSourceKey("slave");
        try {
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceHolder.setDataSourceKey(oldKey);
        }
    }
}

// Service 层使用
@Service
public class UserService {
    @ReadOnly // 标记走从库
    public User getUser(Long id) {
        return userMapper.findById(id);
    }
}

总结

通过以上具体场景的优化策略,可显著提升 MyBatis 性能。关键点:

  1. SQL 层面

    • 避免 SELECT *,强制索引,优化分页
    • 减少动态 SQL 嵌套复杂度
  2. 架构层面

    • 批量操作使用 Batch 模式
    • 合理配置连接池参数
    • 读写分离降低主库压力
  3. 监控层面

    • 开启慢查询日志
    • 使用 p6spy 分析真实 SQL

根据实际业务监控结果(如 APM 工具),针对性调整优化策略。