java反射调用及泛型擦除引起的mySql索引失效

一、背景

        生产环境发生了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>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值