最近一段时间开发工作中,发现了一些mybatis使用的技巧或注意事项,在这里总结一下。
最近由于项目优化,我们需要下线几个数据表,为了安全的移除对几张数据表的依赖,下线过程分成了两个步骤:一是移除对几张表的查询依赖,二是停止对几张表的写入操作。
对几张表的操作使用了mybatis,涉及的方法较多,而且涉及业务系统的核心链路,稍有不慎可能会阻塞核心链路,导致P级故障。
在完成以上两个步骤的开发工作后,为了监控对几张表的使用情况,我们把每个表的Mapper方法访问做了日志埋点,然后基于日志信息绘制监控图表,直观查看每个方法的访问情况。这里使用的就是mybatis的拦截器(Interceptor)。
@Component
@Intercepts(value = {
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class MapperMethodInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
if (target instanceof Executor) {
// 获取当前执行的MappedStatement
MappedStatement mappedStatement = (MappedStatement)invocation.getArgs()[0];
// 完成的id
String id = mappedStatement.getId();
// 对应的类名
String mapperName = id.substring(0, id.lastIndexOf("."));
// 方法名
String methodName = id.substring(id.lastIndexOf(".")+1);
// 监控日志埋点
MetricUtils.log("MapperMethodMetric", Arrays.asList(mapperName, methodName));
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
复制代码
之前,每次做线上发布时都会出现一波系统抖动,钉钉报警铺天盖地而来:
究其原因,发现是:
这里我们关注向DB写入已存在数据时触发唯一约束异常的问题。在DB设计中,为了保证业务的唯一性,我们通常设置唯一索引(unique key),帮助我们在DB层面做好约束。当有重复数据插入时,DB就会写入失败,体现在代码中就是抛出异常。示例如下:
# tb_test存在唯一索引 unique(name)
insert into tb_test(name,phone,address) values ('abcd','13900000001','aaa');
# name=abcd已存在,执行下面的语句就会报错
insert into tb_test(name,phone,address) values ('abcd','13900000001','aaa');
复制代码
按照幂等设计的原则,当有重复请求执行时,系统应该返回幂等成功,否则上游系统可能会一直重试,系统也会一直报错。
其实,在一定程度上解决这个问题比较简单:每次写入数据前,先按照唯一索引进行查询,确定数据库不存在后再执行写入。但是,如果存在并发请求,该方式也存在概率性失败;而且,先查询后写入,也会多一次DB请求,性能有损。
这里还有一种优雅的解决方式,就是insert ignore ...
。来看下官方对它的解释:
If you use the IGNORE modifier, ignorable errors that occur while executing the INSERT statement are ignored.
翻译:如果使用IGNORE修饰符,在执行INSERT语句时遇到错误将被忽略。
For example, without IGNORE, a row that duplicates an existing UNIQUE index or PRIMARY KEY value in the table causes a duplicate-key error and the statement is aborted.
翻译:举例来说,如果不使用IGNORE修饰符时,当新插入的数据在表中存在唯一索引或者重复主键时,将触发“重复键”错误,当前的执行语句会被终止。
With IGNORE, the row is discarded and no error occurs. Ignored errors generate warnings instead.
翻译:如果使用了IGNORE修饰符,该“重复键”错误将被忽略,取而代之的是一个警告(warning)。
所以上面的例子可以修改为:
# tb_test存在唯一索引 unique(name)
insert ignore into tb_test(name,phone,address) values ('abcd','13900000001','aaa');
# 使用ignore修饰符,不会报错
insert ignore into tb_test(name,phone,address) values ('abcd','13900000001','aaa');
复制代码
再介绍一种写入方式:replace into
,官方解释如下:
REPLACE works exactly like INSERT, except that if an old row in the table has the same value as a new row for a PRIMARY KEY or a UNIQUE index, the old row is deleted before the new row is inserted.
翻译:REPLACE很像INSERT,但是如果表中存在与待写入数据相同的主键或唯一索引的旧记录时,它会把旧记录删除后再执行写入。
在我们的订单业务中需要根据上游消息频繁的更新订单状态,由于订单状态机的控制不在我方系统,我们需要做到订单状态的有序变更并保证最终一致性。状态流转如下图所示:
目标:
考虑到:
我们采用了如下处理方式:
也就是:更新之前先查询出记录,通过代码检查记录是否满足更新的条件,然后以待更新的记录主键id、版本号作为条件执行更新。类似的sql如下:
update tb_order
set order_status = #{status}, version = #{newVersion}
where id = #{id} and version = #{oldVersion}
复制代码
数据更新是我们开发人员每天面对的操作,对于状态驱动的数据记录,如何做到安全的更新数据记录是非常重要的。根据实际的工作经验,目前能总结如下几条:
mybatis采用ONGL
实现了动态sql机制,归功于其强大的语法能力,世面上出现了很多代码生成器,也导致我们的项目充斥着各种万能sql。
不得不说,万能sql使得我们的开发效率大大提升,对于一般的小项目,代码生成后我们基本不用考虑dao层的代码编写。
但是,万能sql也是一把双刃剑,稍有不慎就会酿成大错,我们也曾经因万能sql使用不当造成了P2“惨案”。
举个例子(直接拿官方的示例进行修改):
复制代码
假设BLOG数据表包含索引index(state,category,title)
,该表含有500万数量。
如果每次查询都包含条件state
,应该不会有什么问题。
但是针对条件state
、category
、title
,每次使用0至3个都是可以的,这条sql可以三个条件的全组合。
假设在后续工作中,某个同学在变更中仅仅需要使用title
做条件,发现这个sql只需要传递title
参数,其他参数为空就能满足,索性直接复用。
在忘记加对应的索引情况下,直接使用发布上线。你猜会发生什么?
慢sql、db打满、应用故障、系统雪崩……
回过头来看,我们应该怎么做才能避免类似的问题呢?这里我们也总结了几条经验:
比较安全的写法如下,这样state
参数将作为必填参数,即使漏传也只是导致sql错误,不会酿成大祸。
复制代码
以上几个小点是在最近工作中遇到的问题,看似都微不足道,其实每个都细节满满。写下来,做总结做参考,也希望对大家有帮助。