MyBatis真没那么难,别被面试官吓到了
上周五晚上十点半,我还在公司改一个用户增长实验的SQL逻辑。成都这会儿早就夜深人静了,办公室只剩我和隔壁组一个运维兄弟——他说他在等K8s上一个Pod自动恢复,结果等到花儿都谢了。我俩一边啃泡面一边吐槽:为啥现在连做用户增长都要懂底层数据层?但现实就是这么骨感。
我是小红书干了两年推荐算法的工程师,按理说应该天天和Embedding、CTR预估打交道。可实际上,我们组去年开始搞“端到端增长闭环”,不仅要设计策略,还得自己写接口、查数据、调SQL。产品经理一句“这个漏斗能不能再细化一点”,背后就是一堆DAO层代码要重写。于是MyBatis,这个我大学只在教材里见过的名字,硬生生被我用成了日常工具。
今天这篇,不讲八股文,就聊聊我在实战中怎么把MyBatis从“黑盒”变成“趁手的刀”。尤其如果你正在求职,或者刚入职发现老项目全是MyBatis XML,别慌,看完这篇至少能挺过第一个CR(Code Review)。
为什么不是JPA?也不是裸JDBC?
先说个扎心事实:国内大厂,尤其是业务驱动型的公司(比如我们小红书),MyBatis的统治力远超Spring Data JPA。不是JPA不好,而是它太“理想化”了。一旦你的SQL需要动态拼接、复杂关联、或者要针对MySQL做极致优化,JPA那套“对象映射万能论”就容易翻车。
举个真实例子:我们做用户召回实验时,要根据设备型号、地域、活跃天数等10+个维度动态过滤用户。用JPA写Specification?写到一半我就想砸键盘。而MyBatis的<if>、<foreach>标签,配合XML,三行搞定。
再说裸JDBC?拜托,2024年了,谁还手动写Connection.prepareStatement()?又不是在写区块链节点同步逻辑(虽然我们组还真有人在搞链上行为分析,但那是另一个故事了)。
所以结论很现实:MyBatis = 控制力 + 灵活性 + 国内生态支持。求职面试问它,不是因为多高深,而是因为它代表了你对“数据怎么来的”有基本掌控力。
起手式:一个最简Demo跑起来
别一上来就看源码!先让代码跑起来才有信心。假设你有个用户表:
CREATE TABLE `user_growth` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`uid` VARCHAR(32) NOT NULL,
`register_day` DATE,
`active_days` INT DEFAULT 0,
PRIMARY KEY (`id`)
);
对应的Java实体:
public class UserGrowth {
private Long id;
private String uid;
private LocalDate registerDay;
private Integer activeDays;
// getter/setter 略
}
然后是MyBatis的核心三件套:
- Mapper接口(定义方法)
- XML映射文件(写SQL)
- 配置注册(告诉Spring在哪)
// UserGrowthMapper.java
@Mapper
public interface UserGrowthMapper {
UserGrowth selectByUid(@Param("uid") String uid);
void insert(UserGrowth user);
}
<!-- UserGrowthMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaohongshu.growth.mapper.UserGrowthMapper">
<select id="selectByUid" resultType="com.xiaohongshu.growth.model.UserGrowth">
SELECT * FROM user_growth WHERE uid = #{uid}
</select>
<insert id="insert" parameterType="com.xiaohongshu.growth.model.UserGrowth">
INSERT INTO user_growth(uid, register_day, active_days)
VALUES (#{uid}, #{registerDay}, #{activeDays})
</insert>
</mapper>
注意几个坑:
namespace必须是接口全路径#{}是预编译占位符,防SQL注入;${}是直接拼接,慎用!resultType写全类名,或者配别名(但新手别折腾别名,容易迷路)
在Spring Boot里,只要加个@MapperScan("com.xiaohongshu.growth.mapper"),剩下的MyBatis-Spring-Boot-Starter全包了。
实战踩坑:动态SQL与性能陷阱
去年双11前,我们临时加了个需求:根据运营配置的标签组合筛选高潜用户。产品经理给了个Excel,里面有“近7天登录 + 安卓 + 成都 + 曝光>100”这种规则。
我一开始图快,写了N个if-else拼SQL,结果上线后数据库CPU飙到90%。DBA半夜打电话骂我:“你这是要把主库干挂?”
后来改成MyBatis的<where> + <if>:
<select id="selectHighPotentialUsers" resultType="UserGrowth">
SELECT * FROM user_growth
<where>
<if test="minActiveDays != null">
AND active_days >= #{minActiveDays}
</if>
<if test="city != null and city != ''">
AND city = #{city}
</if>
<if test="platformList != null and platformList.size > 0">
AND platform IN
<foreach collection="platformList" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
</where>
</select>
关键是<where>会自动去掉开头的AND,避免语法错误。而<foreach>处理IN查询,安全又简洁。
但注意:IN列表太大(比如上万ID)会拖垮MySQL。我们后来加了分页拆批处理,每次查1000个,用CompletableFuture并行拉取——这才是真正的“用户增长工程化”。
和云原生/K8s有啥关系?
你可能会问:你不是熟悉K8s吗?MyBatis和容器有毛关系?
还真有!我们在K8s上部署服务时,数据库连接池是关键资源。MyBatis本身不管理连接,靠的是底层DataSource(比如HikariCP)。
曾经有个事故:服务扩容到50个Pod,每个Pod默认Hikari最大连接数10,结果瞬间打爆数据库连接上限(max_connections=200)。DB直接拒绝新连接,整个增长实验中断。
解决方案:
- 限制单Pod连接数:
spring.datasource.hikari.maximum-pool-size=4 - 全局连接监控:用Prometheus抓取
hikaricp_connections_active指标 - 优雅下线:Pod终止前,先调
/actuator/shutdown,让MyBatis释放连接
所以说,写好DAO只是第一步,生产环境的稳定性才是工程师的终极考题。
求职建议:别只会写CRUD
现在很多校招生简历写着“熟悉MyBatis”,结果一问二级缓存原理就懵。其实面试官不指望你背源码,但至少得知道:
| 问题 | 初级回答 | 高级回答 |
|---|---|---|
| MyBatis如何防止SQL注入? | 用#{} | #{}底层是PreparedStatement占位符,${}是字符串拼接,仅用于动态表名/列名 |
| 一级缓存和二级缓存在哪? | 不清楚 | 一级缓存是SqlSession级别,默认开启;二级缓存是Mapper级别,需显式开启,且要求实体可序列化 |
| 批量插入怎么优化? | 循环insert | 用<foreach>拼INSERT VALUES (...), (...), ... 或者用MyBatis-Plus的saveBatch |
另外,结合业务场景谈优化,比背八股文强一百倍。比如:“我在做用户行为日志入库时,用MyBatis的ExecutorType.BATCH模式,将10万条记录插入从3分钟降到8秒。”
最后说两句
MyBatis真的不难,它就是一个让你既享受ORM便利,又保留SQL控制权的工具。我在小红书这两年,从算法岗被迫“全栈化”,反而让我更理解数据流的全链路——毕竟,再牛的推荐模型,也得靠靠谱的DAO层喂数据。
如果你正在求职,别怕问“基础”问题。能把MyBatis讲清楚的人,往往也懂得权衡、取舍和落地——而这,正是工程师最值钱的能力。
对了,成都今天又下雨了,我准备下班去吃火锅。代码可以慢慢调,但毛肚不能久煮。共勉!

评论 0