一、背景
生产环境发生了timeout现象,上游服务调用我们的dubbo接口发生了超时,根据trace发现是我们查mySql数据库太慢,但诡异的是只有一个上游服务调用我们接口超时,其他上游服务调用正常,经过仔细排查发现,由于这个服务是新上的,他们在调用我们接口时,是通过网关泛化调用,在传参时传入了List<Long>,而我们的接口入参是List<String>,进而引起了mysql隐式转换,带来了索引失效,导致超时。
二、测试模拟
我们有一张mysql测试表t_unify_user,包含两个字段u_id(varchar),created_ts(datetime),数据量为267w
mapper接口为:
对应sql为
运行一次单测,数据库查询操作耗时为269毫秒
由于Java中的泛型是 ‘伪泛型’,程序编译期间会将泛型信息擦除而转变为非泛型类,例如List<String>
和List<Integer>
在编译后擦除了泛型类型只留下了原始类java.util.List,
所以对于上述的mapper接口,它的入参并不区分List<String>和List<Integer>,只是在编写代码时,编译器会做语法检查并拦截。
在现实服务场景中,我们会使用dubbo、hsf的泛化调用功能,在服务端会根据传入的接口,参数列表等信息获取服务句柄,然后进行反射调用,这里的反射调用就会绕过上面所说的编译器拦截,进行方法调用。
例如这里使用反射调用mapper接口,传入List<Long>参数,查询耗时1.5s,是用来的4到5倍,在生产上,我们表的数据量是千万级,查询耗时上升到10s以上,上游调用直接timeout。
三、索引失效
我们可以看到两次单测底层的prepareStatement的parameters分别为String(正常调用),Long(反射调用),由于表字段u_id是varchar类型,当传入Long型时,mysql会做隐式转换,sql变成
select u_id, created_ts from t_unify_user where cast(u_id as bigInt)
in (20000226336,10000003635)
当字段上有函数时,不走索引,进行全表扫描,因此耗时上升很多。
关于mysql隐式转换,官方给的标准是:MySQL :: MySQL 8.0 Reference Manual :: 12.3 Type Conversion in Expression Evaluation,索引失效可以参考:一张图搞懂MySQL的索引失效 - 个人文章 - SegmentFault 思否
四、mySql字符串与数值的转换规律
如果表字段是int,查询传入varchar时,是能够走索引的,这时的隐式转换发生在传入的值上,因为mysql具有如下特性:"当字符串与数值比较时,会将字符串转为数值;当数值与字符串比较时,会将字符串转为数值"。我们看下面两个语句,第一个是字符串与9数值比较,执行sql时将不等号左边的字符串9转换成了数值,cast('10' as tinyint) > 9,因此结果是1(如果是将不等号右边的10转成‘10’的话,那结果应该是1,字符串'9'应该是大于'10')。
第二个是数值与字符串比较,将不等号右边的字符串10转换成了数值,即,9>cast('10' as tinyint),所以结果是0
五、结论与行动
在写mybatis sql时,大家都很熟悉jdbcType和javaType,大多数人认为指定jdbcType后,mybatis会按照jdbcType进行类型转换,再操作数据库,其实不然,官网给出的定义是:
jdbcType适用于insert、update、delete操作可空字段的场景;而javaType适用于与HashMap等泛型集合类映射的场景,显式指定javaType可保障符合预期的行为。
原文见:mybatis – MyBatis 3 | Mapper XML Files
如mybatis官方文档给予的建议,在使用java泛型类如List,HashMap时,映射应该指定具体的javaType,避免一些不可预期影响。
所以我们在sql xml中补充了javaType,约束传入的数据类型
当传入的数据不符合javaType时,会抛出异常,阻断了流程,进而避免一些数据库隐式转换带来的不可预期的影响,如索引失效、长度截断等。
六、规约建议
基于此次生产问题排查,结合实际开发,我们提出一条强制性开发规约。
【强制】在mybatis sql中,查询条件从集合,例如List、HashMap里取参数时,#{}里必须传javaType,避免数据库隐式转换带来的索引失效等不可预期影响。
正例:
<select id="" parameterType="java.util.List" resultMap="">
select u_id, created_ts
from t_unify_user where u_id in
<foreach collection="uids" item="item" open="(" separator="," close=")">
<!-- 这里加入了javaType=String 约束 -->
#{item, javaType=String}
</foreach>
</select>
反例:
<select id="" parameterType="java.util.List" resultMap="">
select u_id, biz_type
from t_unify_user where u_id in
<foreach collection="uids" item="item" open="(" separator="," close=")">
<!-- 这里没有加入javaType=String 约束 -->
#{item}
</foreach>
</select>