MyBatis在Java工程实践中的高效数据访问策略

举报
江南清风起 发表于 2025/07/12 17:07:17 2025/07/12
【摘要】 MyBatis在Java工程实践中的高效数据访问策略 1. 引言:为什么还在谈 MyBatis?在 Spring Data JPA、Hibernate 大行其道的今天,MyBatis 依旧是国内金融、电商、政企系统里事实上的 ORM 霸主。原因无他,SQL 的可控性与性能上限。然而“写 SQL 自由”不等于“随意写 SQL”。工程规模一旦膨胀,Mapper XML 散落各处、动态 SQL ...

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 &gt;= #{start}</if>
        <if test="end != null"> AND create_time &lt;= #{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 万行而不失控。

image.png

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。