MyBatis 基础教程:从被领导“逼着学”到上线不崩,一个滴滴后端的踩坑实录
大家好,我是老张,目前在杭州一家出行公司干后端开发,司龄快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 默认不做驼峰转换!
解决方法有两个:
- 开启全局驼峰:
mybatis.configuration.map-underscore-to-camel-case=true - 手动写 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 个地方能优化:
- 合并多次 update 为单条 SQL
- 用
ON DUPLICATE KEY UPDATE替代先查后插 - 关键路径加索引提示(
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 丢失,分页失效!
解决方案:
- 避免在异步方法中直接使用 PageHelper
- 或者手动传递分页上下文(麻烦)
后来我们干脆自己封装了一个分页工具类,底层用 LIMIT OFFSET,虽然不够优雅,但稳定压倒一切。
书籍 & 求职建议:别只看视频
很多新人问我:“MyBatis 怎么学?”
我说:别只看 B 站教程,那些“30 分钟速成”根本覆盖不了生产环境的复杂场景。
我啃过的两本书,真心推荐:
- 《MyBatis 从入门到精通》(刘增辉著):源码级解析,讲清楚了 Executor、TypeHandler、插件机制
- 《高性能 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