摘要:现在想起来,问题其实很简单,MyBatis 的动态 SQL 没用对。但那会儿我还年轻,觉得能用 OGNL 动态拼接条件,方便快捷,写起来特别带劲。至于什么风险、资源开销、调用方怎么用,我压根没想过。
你写过那种看起来「没什么问题」,但一上线就把自己坑死的代码吗?我写过,而且记忆深刻,那是我人生第一次线上 OOM(内存溢出)事故。
现在想起来,问题其实很简单,MyBatis 的动态 SQL 没用对。但那会儿我还年轻,觉得能用 OGNL 动态拼接条件,方便快捷,写起来特别带劲。至于什么风险、资源开销、调用方怎么用,我压根没想过。
直到那天,用户中心挂了。
用 MyBatis 写过 Mapper 的同学应该知道,动态 SQL 是 MyBatis 的一大特色。可以根据不同条件灵活拼接查询语句,不用像写 JDBC 时那样小心翼翼地手动拼接 SQL,防着空格少了,逗号多了,WHERE 条件顺序错了。
举个最经典的例子:
SELECT * FROM blogAND title LIKE CONCAT('%', #{title}, '%')AND author = #{author}AND status = #{status}这种写法就很优雅,配合 标签,自动去掉开头的 AND、OR,即使所有条件都为空,也不会多写一个 WHERE。灵活、简洁,看起来很完美。
但问题就出在“看起来”。
当年我在一家电商公司做用户中心的开发,写了一个通用查询接口 getUserByConditions,支持用用户名、手机号、昵称、用户编号查询用户。为了复用,搞了个动态 SQL:
SELECT * FROM usersWHERE 1 = 1AND user_name = #{userName}AND mobile = #{mobile}AND nick_name = #{nickName}AND user_id = #{userId}看起来没问题吧?可是上线后,系统三天两头 OOM,频率高得让我以为是服务器内存条松了。
后来排查日志发现,调用这个查询接口时,前端很多请求带的是空字符串或者根本没传参数。
也就是说,SQL 实际上执行的是:
SELECT * FROM users WHERE 1 = 1这直接查整张用户表——而我们用户表有 1000 多万条数据,几个这样的请求一起打进来,Tomcat 分分钟被打趴下,JVM 内存顶不住直接崩溃。
我当时就懵了,心里一句话在回响:“你写的不是接口,是个深坑。”
说到底,这就是我没做好最基本的后端校验。以前总以为前端传的参数“应该没问题”,但现实一次次教育我,永远别指望调用你接口的人“讲武德”。
我现在的原则是:只要是入口参数,不管是 URL 传的还是 JSON 体带的,后端必须校验。
比如用 Spring Boot 的话,可以直接用 @Validated 注解搭配 hibernate-validator 做校验:
@Datapublicclass UserQueryDTO {@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")private String userName;@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")private String mobile;private String nickName;private Long userId;}然后在 Controller 层这样用:
@PostMapping("/query")public List queryUser(@RequestBody @Validated UserQueryDTO dto) {return userService.query(dto);}这个做法既简单,又安全,不但能防止空字符串、非法格式参数,还能提前发现问题。尤其是动态 SQL 场景,一定得防止因为参数不合规导致 SQL 执行异常或性能事故。
回头看我当初那个 getUserByConditions 方法,其实就是“贪”,想用一个方法干所有的事,结果反而出事了。
后面我改了做法:该拆就拆,别心疼写代码。
查手机号就一个 getUserByMobile查昵称就一个 getUserByNickName查用户 ID 当然得用主键,不能用通用条件去查你可能觉得这样写麻烦,每次都写一堆类似的 SQL、Mapper。没错,是麻烦。但麻烦的是“你写代码”,而不是“线上炸服务”。
而且现在代码生成工具很成熟,你可以自己写模板,自动生成细颗粒度的 Mapper 和 SQL,保证复用代码的同时,也不牺牲系统的健壮性。
“方便开发”从来不该是“牺牲系统稳定性”的理由。
说到底,动态 SQL 没错,关键是你得明白它的“副作用”。想写得安全、健壮,我现在一般会遵守几个原则:
少用 WHERE 1=1用 标签自动处理逻辑,不容易出错,还能防止空查询。条件必须精确匹配,不模糊就别写 LIKE除非真的是搜索场景,否则能用 = 就用 =。空参数默认不参与查询,但别查全表比如像这样加一条保险线:AND user_name = #{userName}
AND 1 = 2
写动态 SQL 的时候,先想:最坏情况是什么?你要是都知道“只要参数为空就可能查全表”,还不加校验,那这坑就是你自己跳的。
以前我总觉得“防御性编程”这种说法有点玄乎,什么“提前防范风险”,“把自己当作最不靠谱的调用方”——听着挺高大上,真落到代码里,哪有时间管那么多?
直到经历那次线上事故,我终于理解了这句话的真正含义。
写代码不是写诗,没人要求你优美,但必须靠谱。尤其是做后端,守着用户数据、撑着整个系统稳定性,说白了,就是“别出事”。
而防御性编程,就是逼你时刻去想:
如果这段代码遇到了 null 会怎样?参数不合法我拦了吗?查出来的数据会不会特别大?别人调用我这个方法的时候,有没有可能用错?这些不是多此一举,而是帮你提前踩雷。
我的第一次线上 OOM,虽然挺丢人的,但也帮我长记性。现在我每次写动态 SQL 都会很警觉,总是反复去问自己:
“有没有校验?有没有兜底?参数不对会不会查全表?”
事故虽然不好,但如果它能改变你的思维方式,变成你写代码时脑袋里的一道红线,那它就值了。
代码从来不是“写完就完事儿”,而是“写对、写稳、写安全”。
写程序,和玩游戏一样,要有意识——你看小地图,对面打野不见了,你知道该撤退。同理,你写动态 SQL,调用方参数可能为空,你就该有反应。
不然,系统一挂,背锅的可是你自己。
来源:暗夜随风