以下是一些 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>
关键步骤:
-
创建索引:
CREATE INDEX idx_users_email ON users(email);
-
使用
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 性能。关键点:
-
SQL 层面:
- 避免
SELECT *
,强制索引,优化分页 - 减少动态 SQL 嵌套复杂度
- 避免
-
架构层面:
- 批量操作使用 Batch 模式
- 合理配置连接池参数
- 读写分离降低主库压力
-
监控层面:
- 开启慢查询日志
- 使用 p6spy 分析真实 SQL
根据实际业务监控结果(如 APM 工具),针对性调整优化策略。