一次线上时间差1秒的调查

今天在线上查看数据的时候,发现两个datetime时间字段取duration的时候差了一秒。

mysql字段

mysql时间字段都是datetime类型,小数点都是0。两个字段分别是2024-04-19 23:58:16和2024-04-19 23:58:24,时间差是7,正确的时间差应该是8.

查看时间差代码

使用Duration的between静态方法,输入的两个时间都去除了小数点。

1
2
Duration between = Duration.between(time1.withNano(0), time2.withNano(0));
long betweenSeconds = between.getSeconds();

查看时间保存代码

由于上述时间差的代码没有问题,那么只有可能时间字段保存有错误。想了一下,可能是由于小数点的问题,时间加了1秒,写了一个demo验证下猜想是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test() {
WorkOrderDetailFixData workOrderDetailFixData = workOrderDetailFixDataMapper.selectById(225222968040494214L);

// debug now是2024-11-22 11:24:31.666365112
workOrderDetailFixData.setUpdateTime(LocalDateTime.now());
// 数据库中是2024-11-22 11:24:32
this.workOrderDetailFixDataMapper.updateById(workOrderDetailFixData);

log.info("time_diff = " + associateService.timeDiff(workOrderDetailFixData.getPreStationOutTime(),
workOrderDetailFixData.getOutTime()));
}

PS: java中时间小数点最多是9位,mysql是6位。

全局修改

知道问题后,首先check下该服务的数据库中所有的时间字段是什么类型,小数点几位,

查看了所有表,全部是datetime,并且小数点都是0。

这样的话,我们只需要修改LocalDateTime的typeHandler即可,找了一圈,没有发现可以一次性修改的的扩展方式。

于是只能把源代码复制到src中,修改setNonNullParameter方法,去掉小数点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package org.apache.ibatis.type;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Objects;

/**
* @author caszhou
* @date 2024/11/22
*/
public class LocalDateTimeTypeHandler extends BaseTypeHandler<LocalDateTime> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType)
throws SQLException {
if (Objects.nonNull(parameter)) {
// 把小数点去掉
ps.setObject(i, parameter.withNano(0));
} else {
ps.setObject(i, null);
}
}

@Override
public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getObject(columnName, LocalDateTime.class);
}

@Override
public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getObject(columnIndex, LocalDateTime.class);
}

@Override
public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getObject(columnIndex, LocalDateTime.class);
}
}

复制的源代码如何生效

java的Classloader是不会加载重复的class文件,只会加载先找到的先加载,由于在src下,会优先于jar的加载

classpath确认

发布前,我们查看启动脚本中的classpath,由于我们使用的是jib的插件,启动的classpath在jib-classpath-file中。

1
/app/resources:/app/classes:/app/libs/ams-service-adapter-0.1.181-SNAPSHOT.jar:......

我们看到classes在jar之前,这样确保typeHandler能够正确load我们重写的类。

结论

上线后,观察数据,bug修复。

这样的修复方法比较适合数据库时间字段全部是小数点是0的情况,否则就自定义一个typeHandler,手动引入该handler, 以mybatis plus为例。

1
2
3
4
5
@TableName(autoResultMap = true)
public class Process extends BasePo {
@TableField(typeHandler = ListStringToJsonTypeHandler.class)
private List<String> collectKey;
}