MyBatis 基础教程:从被领导“逼着学”到上线不崩,一个滴滴后端的踩坑实录

独立开发路上
2025-12-14 00:41
阅读 584

大家好,我是老张,目前在杭州一家出行公司干后端开发,司龄快4年了。主要负责司机端的核心链路——简单说就是你叫车时背后那些调度、派单、状态同步的逻辑,基本都在我这摊子上。早起型选手,每天8点不到就坐在工位上敲代码(不是卷,是家里娃太闹腾,早点溜出来能多活两小时)。

上周五晚上,项目刚过完一轮压测,我正准备关电脑溜去吃片儿川,leader突然甩过来一条消息:“下个版本要用 MyBatis 重构旧 DAO 层,你牵头搞一下。”
我当时内心OS:不是……咱不是一直用 Spring Data JPA 吗?虽然它有时候慢得像蜗牛爬坡,但至少 CRUD 不用手写 SQL 啊!

但转念一想——跳槽季快到了,阿里网易这边面试八成会问 MyBatis 底层原理,再不系统补一波,简历都过不了 HR 筛。于是咬咬牙,接了这活。

今天这篇,不讲教科书式的“什么是 MyBatis”,也不堆概念。我就用自己这一个月踩过的坑、熬过的夜、改过的 bug,带大家真实体验一把 MyBatis 从入门到上线的过程。顺便安利几本让我少走弯路的书,求职党可以抄作业。


起因:JPA 在高并发下单场景下“翻车”

先说背景。我们司机端有个核心接口叫 acceptOrder,司机点击接单后,要更新订单状态、写入操作日志、触发调度算法……一套流程下来,涉及 5 张表 + 3 次事务提交

之前用的是 Spring Boot + Spring Data JPA,本地跑单元测试稳如老狗。但去年双11大促期间,QPS 飙到 3000+,数据库 CPU 直接干到 95%,报警电话打得我手机发烫。

DBA 抓包一看:JPA 自动生成的 SQL 又臭又长,还带 N+1 查询。比如查司机信息顺带查他最近10单,结果每单又去查乘客信息——一个接口打出 20+ 条 SQL,DB 直接原地升天。

运维小哥在群里阴阳怪气:“你们后端是不是以为数据库是永动机?”
我默默回了个 😅,心里却知道:是时候换 ORM 框架了。


入门 MyBatis:别被 XML 劝退

说实话,第一次看 MyBatis 的 Mapper.xml,我有点懵。这玩意儿不是十年前的老古董吗?现在都 2024 年了,怎么还要手写 SQL?

但真上手才发现:可控性才是性能优化的第一步

第一个坑:ResultMap 映射不对,返回 null 到怀疑人生

我把旧的 JPA Entity 改成 MyBatis 的 POJO,写了个简单查询:

<select id="selectDriverById" resultType="Driver">
    SELECT id, name, phone FROM driver WHERE id = #{id}
</select>

结果调用 driver.getName() 返回 null。
查了半天,发现数据库字段是 driver_name,Java 属性是 name——MyBatis 默认不做驼峰转换

解决方法有两个:

  1. 开启全局驼峰:mybatis.configuration.map-underscore-to-camel-case=true
  2. 手动写 ResultMap(推荐复杂对象)
<resultMap id="DriverMap" type="Driver">
    <id property="id" column="id"/>
    <result property="name" column="driver_name"/>
    <result property="phone" column="mobile"/>
</resultMap>

💡 经验:简单表用驼峰,关联查询一定用 ResultMap。否则后期加字段容易翻车。


性能优化:手写 SQL 的“甜头”与“苦头”

MyBatis 最大的优势是什么?SQL 你说了算

我们把那个高频的 acceptOrder 接口拆解后,发现有 3 个地方能优化:

  1. 合并多次 update 为单条 SQL
  2. ON DUPLICATE KEY UPDATE 替代先查后插
  3. 关键路径加索引提示(USE INDEX

举个例子,原来 JPA 要先查司机状态是否空闲,再更新订单。现在直接:

<update id="acceptOrder">
    UPDATE orders 
    SET driver_id = #{driverId}, status = 'ACCEPTED'
    WHERE order_id = #{orderId} 
      AND EXISTS (
        SELECT 1 FROM drivers 
        WHERE id = #{driverId} AND status = 'IDLE'
      )
</update>

一次网络往返 + 一次事务,搞定。

但手写 SQL 也有代价:SQL 注入风险
千万别这么写:

<!-- 千万别! -->
<select id="searchOrders" resultType="Order">
    SELECT * FROM orders WHERE driver_id = ${driverId}
</select>

${} 是字符串拼接,#{} 才是预编译参数。我第一次上线就因为图省事用了 ${},被安全扫描打回来三次,差点被安全组拉黑。


缓存机制:二级缓存是个“双刃剑”

MyBatis 有两级缓存:

  • 一级缓存:SqlSession 级别,默认开启
  • 二级缓存:Mapper 级别,需手动配置

我一开始兴奋地给所有 Mapper 开了二级缓存,结果第二天线上报警:司机看到的订单状态延迟了 10 分钟

原因很简单:二级缓存是跨 SqlSession 的,但我们的服务是多实例部署。A 机器更新了数据,B 机器还在读缓存——脏读了

后来和 DBA 讨论,决定:

  • 只对几乎不变的数据开二级缓存(比如城市配置表)
  • 核心业务(订单、司机状态)禁用二级缓存,靠 Redis 做分布式缓存

配置也很简单:

<!-- mapper.xml 中关闭 -->
<cache eviction="LRU" size="1024" readOnly="true" blocking="true" />

但我们最终删掉了这行——宁可多查一次 DB,也不能让用户看到错误状态


分页插件:PageHelper 的“暗坑”

分页谁不用?我引入 pagehelper-spring-boot-starter,三行代码搞定:

PageHelper.startPage(1, 10);
List<Order> orders = orderMapper.selectAll();

本地测试完美。但上线后,偶尔出现 total=0 但 list 有数据的诡异现象。

查源码才发现:PageHelper 是通过 ThreadLocal 存储分页参数的。如果在异步线程里调用 Mapper(比如用 @Async),ThreadLocal 丢失,分页失效!

解决方案:

  1. 避免在异步方法中直接使用 PageHelper
  2. 或者手动传递分页上下文(麻烦)

后来我们干脆自己封装了一个分页工具类,底层用 LIMIT OFFSET,虽然不够优雅,但稳定压倒一切


书籍 & 求职建议:别只看视频

很多新人问我:“MyBatis 怎么学?”
我说:别只看 B 站教程,那些“30 分钟速成”根本覆盖不了生产环境的复杂场景。

我啃过的两本书,真心推荐:

  1. 《MyBatis 从入门到精通》(刘增辉著):源码级解析,讲清楚了 Executor、TypeHandler、插件机制
  2. 《高性能 MySQL》(第4版):虽然不是讲 MyBatis,但 ORM 最终要落到 SQL 和索引上,这本书救我命

尤其是准备跳槽的同学——阿里 P6 面试必问 MyBatis 插件原理。你能说出 Interceptor 怎么拦截 Executor.query() 吗?能手写一个 SQL 执行时间监控插件吗?

我上个月面网易,就被问到:“MyBatis 一级缓存为什么在 Spring 中失效?”
答:因为 Spring 每次调用 Mapper 都会创建新 SqlSession(通过 SqlSessionTemplate 代理),所以一级缓存形同虚设。
面试官点头:“看来真用过。”


生产经验总结:上线前 checklist

经过这次重构,我整理了一份 MyBatis 上线前必查清单,分享给大家:

检查项 说明 血泪教训
SQL 参数是否全用 #{} 防止注入 曾因 ${} 被安全团队通报
ResultMap 字段是否对齐 避免 null 值 测试环境漏测,线上 NPE
是否禁用不必要的二级缓存 防止脏读 多实例部署必踩
事务边界是否清晰 避免大事务 一次 update 涉及 5 表,锁表 3s
慢 SQL 是否有监控 提前预警 上线后才发现没走索引

另外,一定要做全链路压测。我们用 Arthas 监控 SQL 执行时间,发现某个关联查询平均 80ms,加了复合索引后降到 3ms——这种优化,只有真实流量才能暴露。


写在最后:框架只是工具,理解数据流才是核心

折腾一个月,MyBatis 版本终于上线。QPS 提升 40%,DB CPU 稳定在 60% 以下。产品经理终于不再催“为什么司机接单要等 2 秒”。

回头看,MyBatis 并不是银弹。它的学习曲线比 JPA 陡,维护成本也高。但在对性能、SQL 可控性有强要求的场景下,它几乎是唯一选择

如果你也在杭州,准备冲阿里网易,我的建议是:

  • 别只背八股文,动手写个 MyBatis 插件
  • 别迷信自动生成,理解每条 SQL 的执行计划
  • 别怕手写 SQL,那是你和数据库对话的母语

最后送大家一句我们团队墙上贴的话:“ORM 框架可以换,但对数据的敬畏不能丢。”

好了,片儿川要凉了,我去干饭了。有问题评论区见,看到会回(除非在改 bug)。


作者:老张,滴滴4年后端,坐标杭州,主业写 bug,副业修 bug。正在看机会,求内推阿里/网易后端岗(认真脸)

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝