这就是我不再乱用动态 SQL 的原因,生产事故后悔三年!

B站影视 韩国电影 2025-05-16 17:12 1

摘要:现在想起来,问题其实很简单,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,调用方参数可能为空,你就该有反应。

不然,系统一挂,背锅的可是你自己。

来源:暗夜随风

相关推荐