MyBatis 到底值不值得学?一个滴滴后端老油条的碎碎念
上周五晚上十点半,我家猫正趴在我键盘上打呼噜,我还在改一个线上司机端订单状态同步的 Bug。问题不大,就是某个 MySQL 语句在高并发下偶尔锁表,但排查起来却让我重新翻开了尘封已久的 MyBatis 源码。
我是滴滴的后端开发,干了快四年,主要搞司机端的核心业务——从接单、计价到完单结算,全链路都沾过手。远程办公两年多了,代码写得多,头发掉得也多。团队里新人越来越多,前两天还有个刚转 Java 的同事问我:“哥,现在都 2024 年了,还用 MyBatis?Spring Data JPA 不香吗?甚至有人用 Python 写 ORM……”
我笑了笑,没直接回答。但那天晚上修完 Bug 后,突然觉得:是时候聊聊 MyBatis 这个“老古董”了。
被面试官问懵的那一刻
其实我最早接触 MyBatis 是在准备跳槽时被“逼”的。那会儿面一家中厂,面试官冷不丁问:
“MyBatis 的一级缓存和二级缓存有什么区别?在分布式环境下用二级缓存会出什么问题?”
我当时支支吾吾说了点皮毛,回来就赶紧补课。结果入职滴滴后发现——我们生产环境压根没开二级缓存!不是不能用,而是不敢用。分布式系统里缓存一致性太难搞,一不小心就脏读,司机看到的订单状态和乘客端对不上,轻则客诉,重则资损。
这事儿让我明白:框架本身不复杂,复杂的是你怎么用它。
MyBatis 到底是个啥?
简单说,MyBatis 是一个 Java 持久层框架,它把 SQL 从 Java 代码里抽出来,放到 XML 或注解里,让你能灵活控制 SQL,又不用手写 JDBC 那堆样板代码。
对比 Hibernate(或者 Spring Data JPA),MyBatis 更“贴近 SQL”,而 Hibernate 更“面向对象”。前者像手动挡,后者像自动挡。在滴滴这种对性能、SQL 可控性要求极高的场景,手动挡反而更稳。
举个真实例子:司机端有个接口叫 getNearbyOrders,要查附近 5 公里内未接单的订单。这个查询涉及空间索引、状态过滤、分页,SQL 很复杂。用 JPA 写?光那个 @Query 注解就能写半屏,还容易生成低效 SQL。而 MyBatis 直接写原生 SQL,配合 <if> 标签动态拼接,清晰又高效。
<select id="selectNearbyOrders" resultType="Order">
SELECT id, passenger_id, start_lat, start_lng, status
FROM orders
WHERE status = #{status}
AND (6371 * acos(cos(radians(#{driverLat}))
* cos(radians(start_lat))
* cos(radians(start_lng) - radians(#{driverLng}))
+ sin(radians(#{driverLat}))
* sin(radians(start_lat)))) <= 5
ORDER BY created_at DESC
LIMIT #{limit}
</select>
看,地理距离计算直接塞进 SQL,数据库一层搞定,比拉到 Java 层算快多了。
动态 SQL:MyBatis 的灵魂
很多人觉得 MyBatis 就是个 SQL 模板引擎,但真正体现功力的是它的 动态 SQL 能力。
比如我们有个司机信用分查询接口,前端传一堆筛选条件:城市、车型、注册时间范围、是否完成过夜间单……这些条件可能有、可能没有。用传统 JDBC 写?得拼字符串,还容易 SQL 注入。
MyBatis 的 <where>、<if>、<foreach> 组合拳一套下来,干净利落:
<select id="queryDriverCredit" resultType="DriverCredit">
SELECT driver_id, credit_score, city, vehicle_type
FROM driver_credit
<where>
<if test="city != null">
AND city = #{city}
</if>
<if test="vehicleTypes != null and vehicleTypes.size() > 0">
AND vehicle_type IN
<foreach item="type" collection="vehicleTypes" open="(" separator="," close=")">
#{type}
</foreach>
</if>
<if test="registerStartTime != null">
AND register_time >= #{registerStartTime}
</if>
</where>
</select>
注意 <where> 标签会自动去掉开头的 AND,避免语法错误。这种细节,才是 MyBatis 被大厂青睐的原因。
缓存陷阱:别被“高性能”忽悠了
前面提过缓存。MyBatis 默认开启一级缓存(SqlSession 级别),同一个 SqlSession 里重复查询会走缓存。听起来很美,但在 Web 应用中,每个请求通常新建一个 SqlSession,所以一级缓存基本失效。
二级缓存是 Mapper 级别的,多个 SqlSession 共享。但问题来了:如果你的服务部署了多个实例(当然会),二级缓存只在本机生效,根本做不到集群共享!除非你外接 Redis 做分布式缓存,但那就不是 MyBatis 自己的事了。
所以我们团队的共识是:关掉二级缓存,自己用 Spring Cache + Redis 控制缓存逻辑。虽然多写几行代码,但可控、可监控、可降级。
和 Python 对比?别闹了
有同事开玩笑说:“你看人家 Python,Django ORM 一行代码搞定增删改查,多优雅!”
我只能回他一句:“你是在写博客,还是在扛双11流量?”
Python 的 ORM 确实简洁,但牺牲了对 SQL 的精细控制。在高并发、低延迟的场景下,ORM 自动生成的 N+1 查询、隐式 JOIN、慢 SQL 防不胜防。而 MyBatis 让你每一条 SQL 都在眼皮底下,出了问题直接看执行计划,定位快如闪电。
去年双11前夕,我们有个聚合查询突然变慢。用 MyBatis 日志一扒,发现是某字段没走索引。加个复合索引,QPS 从 800 直接飙到 5000。要是用全自动 ORM,估计得先翻半天文档才知道怎么 hint 索引。
面试题高频考点整理
既然提到面试,我顺手整理几个 MyBatis 高频题,都是我被问过或问过别人的:
| 问题 | 关键点 |
|---|---|
#{}和${}的区别 |
#{} 预编译防注入,${} 直接替换(慎用) |
resultType 和 resultMap |
简单映射用前者,复杂关系(嵌套、驼峰)用后者 |
如何实现分页? |
用 PageHelper 插件,底层改写 SQL 加 LIMIT |
插件(Plugin)原理 |
基于 JDK 动态代理,拦截 Executor/StatementHandler |
MyBatis 如何防止 SQL 注入? |
主要靠 #{} 的 PreparedStatement 机制 |
特别提醒:别背答案,要理解源码。比如 #{} 为啥安全?因为 MyBatis 最终调用的是 PreparedStatement.setString(),参数被当作值而非 SQL 片段处理。
生产环境踩坑实录
最后分享两个血泪教训:
驼峰命名没开,字段对不上
数据库用user_id,Java 用userId,结果查出来全是 null。解决:全局配置mapUnderscoreToCamelCase=true。批量插入 OOM
用foreach插 10 万条数据,内存爆了。正确做法:分批提交,每 1000 条 flush 一次 SqlSession。
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
OrderMapper mapper = session.getMapper(OrderMapper.class);
for (int i = 0; i < orders.size(); i++) {
mapper.insert(orders.get(i));
if (i % 1000 == 0) {
session.commit();
session.clearCache(); // 清理一级缓存,防内存泄漏
}
}
session.commit();
} finally {
session.close();
}
结语:老框架,新价值
MyBatis 诞生十几年了,有人说它过时。但在我眼里,它就像一把趁手的螺丝刀——不花哨,但关键时刻从不掉链子。
在滴滴这种业务复杂、性能敏感的环境里,我们需要的是可控、透明、高效的工具。MyBatis 恰好满足。它不替你做决定,而是把选择权交给你——这正是专业开发者需要的。
所以,别管什么 Python ORM 多优雅,也别迷信“全自动”。真正的高手,既会用框架,也敢写 SQL。
好了,猫已经把我的咖啡杯打翻了,代码还没 push。这篇文章就写到这儿吧。希望对你有用,至少下次面试别再被缓存问题问住。
(P.S. 如果你在用 MyBatis Plus,那是另一回事了——不过那是下一篇文章的故事了。)

评论 0