MyBatis在Java工程实践中的高效数据访问策略
MyBatis在Java工程实践中的高效数据访问策略
1. 引言:为什么还在谈 MyBatis?
在 Spring Data JPA、Hibernate 大行其道的今天,MyBatis 依旧是国内金融、电商、政企系统里事实上的 ORM 霸主。原因无他,SQL 的可控性与性能上限。
然而“写 SQL 自由”不等于“随意写 SQL”。工程规模一旦膨胀,Mapper XML 散落各处、动态 SQL 难以维护、N+1 查询频发,反而会成为性能陷阱。
本文结合 3 个真实生产案例,给出一条“从 1 张表到 1000 张表”可持续落地的 MyBatis 高效数据访问策略,并给出可直接粘贴运行的代码示例(基于 MyBatis 3.5.14 + Spring Boot 3.2)。
2. 环境准备与基线项目
2.1 Maven 依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
2.2 表结构与数据量
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL,
KEY idx_user (user_id)
) ENGINE=InnoDB;
-- 预置 1000w 条数据
3. 策略一:Mapper 拆分与多数据源路由
痛点:单体服务连接 8 个业务库,Mapper 与 XML 同名覆盖。
3.1 分包隔离
com.example
├─ member
│ ├─ mapper
│ └─ xml
├─ order
│ ├─ mapper
│ └─ xml
└─ config
3.2 动态数据源 + 注解式路由
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouteDataSource {
String value(); // "member" or "order"
}
@Component
@Aspect
public class DataSourceAspect {
@Around("@annotation(route)")
public Object switchDs(ProceedingJoinPoint pjp, RouteDataSource route) throws Throwable {
try {
DynamicDataSourceHolder.set(route.value());
return pjp.proceed();
} finally {
DynamicDataSourceHolder.clear();
}
}
}
3.3 MyBatis 配置
@Configuration
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(dataSource);
fb.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/**/*.xml")
);
org.apache.ibatis.session.Configuration c = new org.apache.ibatis.session.Configuration();
c.setMapUnderscoreToCamelCase(true);
c.setDefaultExecutorType(ExecutorType.REUSE); // 重用 PreparedStatement
fb.setConfiguration(c);
return fb.getObject();
}
}
4. 策略二:动态 SQL 模板化与分页优化
痛点:运营后台 20+ 复合查询条件,XML 中
<where>
标签爆炸。
4.1 用 MyBatis 的 <sql>
片段做列复用
<sql id="Base_Column_List">
id, user_id, amount, status, create_time
</sql>
<sql id="Where_Clause">
<where>
<if test="userId != null"> AND user_id = #{userId}</if>
<if test="statusList != null and statusList.size > 0">
AND status IN
<foreach collection="statusList" item="s" open="(" separator="," close=")">
#{s}
</foreach>
</if>
<if test="start != null"> AND create_time >= #{start}</if>
<if test="end != null"> AND create_time <= #{end}</if>
</where>
</sql>
<select id="search" resultType="com.example.order.entity.Order">
SELECT <include refid="Base_Column_List"/>
FROM t_order
<include refid="Where_Clause"/>
ORDER BY id DESC
LIMIT #{offset}, #{size}
</select>
4.2 PageHelper 的“隐式 count”问题与替代方案
PageHelper 在 count 时会剥离 ORDER BY
,但复杂 SQL 仍可能耗时。
推荐手写 count:
<select id="countSearch" resultType="long">
SELECT COUNT(1) FROM t_order
<include refid="Where_Clause"/>
</select>
Service 层手动分页:
Page<Order> page = Page.of(req.getPageNo(), req.getPageSize());
long total = orderMapper.countSearch(req);
if (total > 0) {
List<Order> rows = orderMapper.search(req, page.offset(), page.size());
page.setTotal(total).setRecords(rows);
}
5. 策略三:二级缓存与 Redis 组合
痛点:秒杀活动中,库存校验 QPS 5w,MyBatis 一级缓存(Session) 无济于事。
5.1 开启二级缓存
<mapper namespace="com.example.order.mapper.OrderMapper">
<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>
</mapper>
5.2 自定义 RedisCache
public class RedisCache implements Cache {
private final String id;
private static final RedisTemplate<String, byte[]> TEMPLATE;
static {
// 初始化 RedisTemplate,使用 Kryo 序列化
}
public RedisCache(String id) { this.id = id; }
@Override
public String getId() { return id; }
@Override
public void putObject(Object key, Object value) {
TEMPLATE.opsForValue().set(key.toString(), KryoUtil.write(value), 5, TimeUnit.MINUTES);
}
@Override
public Object getObject(Object key) {
byte[] bytes = TEMPLATE.opsForValue().get(key.toString());
return bytes == null ? null : KryoUtil.read(bytes);
}
}
5.3 业务层双写一致性
订单状态变更后,删除缓存:
@Transactional
public void paySuccess(long orderId) {
orderMapper.updateStatus(orderId, PAID);
Cache redis = cacheManager.getCache("Order");
redis.evict(orderId);
}
6. 策略四:批量插入与游标流式查询
痛点:每日账单文件 200w 行,JDBC 批处理 30 分钟超时。
6.1 批量插入 rewriteBatchedStatements
@Transactional
public void batchSave(List<Order> list) {
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false);
try {
OrderMapper mapper = session.getMapper(OrderMapper.class);
for (int i = 0; i < list.size(); i++) {
mapper.insert(list.get(i));
if (i % 2000 == 0) {
session.flushStatements();
}
}
session.commit();
} finally {
session.close();
}
}
MySQL 连接串务必加:
jdbc:mysql://...?rewriteBatchedStatements=true
6.2 游标查询避免 OOM
@Select("SELECT * FROM t_order WHERE create_time >= #{start}")
@Options(resultSetType = FORWARD_ONLY, fetchSize = 1000)
@ResultType(Order.class)
void scanLargeData(@Param("start") LocalDateTime start, ResultHandler<Order> handler);
Service 侧:
orderMapper.scanLargeData(start, resultContext -> {
Order o = resultContext.getResultObject();
// 逐条处理,不落内存
});
7. 策略五:MyBatis Generator 的“工程化”改造
7.1 不生成 Example 类,改用 MyBatis-Plus Wrapper
<table tableName="t_order" domainObjectName="Order"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"
enableSelectByExample="false"/>
7.2 自定义插件:统一继承 BaseDO
public class BaseDoPlugin extends PluginAdapter {
@Override
public boolean modelBaseRecordClassGenerated(TopLevelClass top, IntrospectedTable table) {
top.setSuperClass("com.example.common.BaseDO");
return true;
}
}
8. 策略六:MyBatis 与 Spring Boot 3 原生编译
8.1 AOT 提示文件
@RuntimeHints
public class MyBatisHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("mapper/**/*.xml");
}
}
8.2 native-image 参数
--initialize-at-build-time=org.apache.ibatis.ognl.OgnlRuntime
9. 监控与可观测性
9.1 拦截器打印真实 SQL + 耗时
@Intercepts(@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}))
public class SqlCostInterceptor implements Interceptor {
@Override
public Object intercept(Invocation inv) throws Throwable {
StatementHandler sh = (StatementHandler) inv.getTarget();
BoundSql boundSql = sh.getBoundSql();
long start = System.nanoTime();
try {
return inv.proceed();
} finally {
log.info("SQL: {} cost {} ms",
boundSql.getSql().replaceAll("\\s+", " "),
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
}
}
}
9.2 Prometheus 指标
使用 mybatis-plus-boot-starter
内置的 PerformanceInterceptor
(3.5.x 已标记废弃,可自研 Micrometer Binder)。
10. 结语:可演进的 MyBatis 架构
阶段 | 核心目标 | 关键动作 |
---|---|---|
单库 | 快 | 代码生成器 + PageHelper |
分库 | 稳 | 多数据源 + 动态 SQL |
高并发 | 省 | 二级缓存 + 批量 + 游标 |
云原生 | 轻 | GraalVM AOT + 可观测 |
MyBatis 的“轻量”是把双刃剑:
- 用得好,SQL 即性能;
- 用得差,XML 即债务。
遵循以上六大策略,可让 MyBatis 项目从 1 万行平滑支撑到 1000 万行而不失控。
- 点赞
- 收藏
- 关注作者
评论(0)