MyBatis作为一款优秀的持久层框架,凭借其灵活性和易用性在Java开发领域广受欢迎。本文将深入探讨MyBatis的性能优化策略,帮助开发者构建高效稳定的数据访问层。
一、MyBatis的优化方向
MyBatis的优化方向有以下几个方面:SQL语句优化、缓存机制优化 、批量操作优化、结果集处理优化、高级特性优化、性能监控与调优,下面就需要详细讲一下这些方向。
二、SQL语句优化
1、避免使用SELECT (* )
1.1 问题分析
- 数据传输冗余:返回未使用的字段浪费网络贷款和内存资源。
- 扩展性问题:表结构变更可能导致映射错误。
- 索引失效:若使用覆盖索引,select * 会强制回表查询。
1.2 优化方案
明确指定字段列表,仅获取必要数据
<select id="getUserBasic" resultType="User">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
1.3 动态字段选择
通过<if>动态控制返回字段(适用于不同场景字段需求差异大的情况):
<select id="getUser" resultType="User">
SELECT
<choose>
<when test="isBasic == true">id, username</when>
<otherwise>*</otherwise>
</choose>
FROM users WHERE id = #{id}
</select>
2、索引优化策略
2.1 索引命中原则
-
最左前缀匹配:联合索引
(a,b,c)
仅对a
、a,b
、a,b,c
生效。 -
避免索引失效场景:
-
对索引字段使用函数或表达式:
WHERE YEAR(create_time) = 2023
-
类型不匹配:字符串字段用数字查询
-
LIKE
以通配符开头:WHERE name LIKE '%abc'
-
2.2 执行计划分析
使用EXPLAIN命令查看查询执行计划:
EXPLAIN SELECT * FROM users WHERE age > 20;
关注关键指标:
-
type:访问类型(const > ref > range > index > ALL)
-
key:实际使用的索引
-
rows:扫描行数
-
Extra:Using filesort、Using temporary等警告
2.3 索引优化示例
场景:用户表分页查询优化
原始SQL:
SELECT * FROM users ORDER BY create_time DESC LIMIT 100000, 10;
问题:深度分页导致全表扫描
优化方案:
-
延迟关联:先查主键,再回表
SELECT * FROM users u1
INNER JOIN (SELECT id FROM users ORDER BY create_time DESC LIMIT 100000, 10) u2
ON u1.id = u2.id;
-
游标分页:基于上一页最大ID查询
SELECT * FROM users
WHERE create_time < #{lastCreateTime}
ORDER BY create_time DESC LIMIT 10;
3. JOIN优化技巧
3.1 关联字段索引
确保JOIN条件的字段有索引:
-- 为department表的id建立索引
ALTER TABLE department ADD INDEX idx_id (id);
SELECT u.name, d.dept_name
FROM users u
LEFT JOIN department d ON u.dept_id = d.id;
3.2 小表驱动大表
-
原则:用数据量小的表作为驱动表
-
示例:当部门表数据远小于用户表时:
SELECT /*+ STRAIGHT_JOIN */ d.dept_name, u.name
FROM department d
LEFT JOIN users u ON d.id = u.dept_id;
通过STRAIGHT_JOIN
强制指定驱动表顺序。
3.3 子查询转JOIN
低效写法:
SELECT * FROM users
WHERE dept_id IN (SELECT id FROM department WHERE status = 1);
优化方案:
SELECT u.* FROM users u
INNER JOIN department d ON u.dept_id = d.id
WHERE d.status = 1;
4. 动态SQL优化
4.1 <where>
标签智能处理
自动去除首尾多余的AND/OR,避免语法错误:
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">AND name LIKE #{name}</if>
<if test="age != null">AND age = #{age}</if>
</where>
</select>
生成SQL:
-
当所有条件为空时:
SELECT * FROM users
-
当只有age存在时:
SELECT * FROM users WHERE age = ?
4.2 避免动态SQL陷阱
-
过度嵌套问题:超过3层的
<if>
嵌套应重构为多个独立查询。 -
参数空值处理:使用
<if test="param != null">
避免无效条件。
4.3 使用<script>
标签(MyBatis 3.2+)
在注解Mapper中编写动态SQL:
@Select("<script>" +
"SELECT * FROM users " +
"<where>" +
" <if test='name != null'>AND name = #{name}</if>" +
"</where>" +
"</script>")
List<User> findUsers(@Param("name") String name);
5. 预编译与参数绑定
5.1 #{}与${}的区别
-
#{}:参数占位符,生成预编译语句(PreparedStatement),防止SQL注入。
-
${}:字符串替换,直接拼接到SQL中,适用于动态表名/列名。
正确用法示例:
<!-- 安全的分表查询 -->
<select id="getLogs" resultType="Log">
SELECT * FROM logs_${tableSuffix} WHERE create_time > #{time}
</select>
5.2 参数类型处理
-
日期类型处理:指定jdbcType避免类型推断错误
<insert id="insert">
INSERT INTO orders(create_time)
VALUES(#{createTime, jdbcType=TIMESTAMP})
</insert>
6. 批处理优化
6.1 批量插入(foreach方式)
<insert id="batchInsert">
INSERT INTO users (name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
注意:
-
单次批量插入数据量建议控制在500-1000条以内,避免超长SQL。
-
MySQL的
max_allowed_packet
参数限制单个包大小(默认4MB)。
6.2 批量更新(CASE WHEN方式)
UPDATE users
SET age = CASE
WHEN id = 1 THEN 25
WHEN id = 2 THEN 30
END
WHERE id IN (1, 2);
MyBatis实现
<update id="batchUpdate">
UPDATE users
SET age =
<foreach collection="list" item="user" open="CASE" close="END">
WHEN id = #{user.id} THEN #{user.age}
</foreach>
WHERE id IN
<foreach collection="list" item="user" open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
7. SQL注入防御
7.1 预编译强制使用
-
全局配置:在
mybatis-config.xml
中禁用不安全操作:
<settings>
<setting name="useActualParamName" value="false"/>
<!-- 禁止${}方式传递普通参数 -->
</settings>
运行 HTML
7.2 危险字符过滤
在Java层对输入参数进行过滤:
public void setUsername(String username) {
if (username.matches(".*[\\\\/'\"].*")) {
throw new IllegalArgumentException("Invalid username");
}
this.username = username;
}
7.3 白名单校验
动态表名/列名场景:
// 校验表名是否合法
private boolean isValidTableName(String tableName) {
return Arrays.asList("user", "order").contains(tableName);
}
// Mapper接口
@Select("SELECT * FROM ${tableName}")
List<Map<String, Object>> selectFromTable(@Param("tableName") String tableName);
总结
SQL优化需要结合具体场景,通过以下步骤系统实施:
-
分析慢查询日志:定位高频低效SQL
-
EXPLAIN解析执行计划:识别全表扫描、临时表等问题
-
索引优化:增加缺失索引,删除冗余索引
-
重写复杂SQL:拆分子查询,优化JOIN顺序
-
压力测试验证:使用JMeter等工具验证优化效果