【当心】一次日期格式转化的线上事故

1,745 阅读3分钟

「这是我参与11月更文挑战的第32天,活动详情查看:2021最后一次更文挑战」。

原创不易,望多多关注、多多点赞🙇‍👍

事故描述

公司的app客户端会上报一些用户数据到Java后台服务,其中有一个点击时间的字段。今天在巡查日志的时候,发现了大量该保存该字段是的error日志。

如下:

Data truncation: Incorrect datetime value: '53884-04-07 04:09:44' for column 'clickTime' at row 1

伪代码

/**
 * 根据日期格式DateTime转String
 */
public static String dateTimeMillisToString(long time, String pattern) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(time);
    return (new SimpleDateFormat(pattern)).format(calendar.getTime());
}

/**
 * 保存客户端上报的用户数据
 */
public void save(User user) {
    // 注意这里要将 时间*1000 转换成毫秒数
    String time = dateTimeMillisToString(user.getClickTime() * 1000, "yyyy-MM-dd HH:mm:ss");
    user.setCreateTime(time);
    save(user);
}

猜想

根据异常日志和源代码,我们猜想可能是因为有些客户端没有按原定的以为单位来上报,而是使用的毫秒为单位。

为了验证猜想,决定写个main方法验证一下。

现场还原

public static void main(String[] args) {
    long time1 = 1638263956L;
    long time2 = 1638263956000L;
    System.out.println(dateTimeMillisToString(time1 * 1000, "yyyy-MM-dd HH:mm:ss"));
    System.out.println(dateTimeMillisToString(time2 * 1000, "yyyy-MM-dd HH:mm:ss"));
}

结果和预想的一样,果然是因为毫秒的问题。

image.png

解决问题

String time = user.getClickTime();
if (StringUtils.isNotBlank(time)) {
    if (time.length() == 10) {
        // 10位,表示该时间以秒为单位
        time = dateTimeMillisToString(time * 1000, YYYYMMDD_HHMMSS);
    } else if (time.length() == 13) {
        // 13位,表示该时间以毫秒为单位
        time = dateTimeMillisToString(time, YYYYMMDD_HHMMSS);
    }
}

你以为这样就结束了吗?

修复好发生产后,却爆发了更多的异常,量级是原来的十多倍,我一下子慌了神,赶紧找运维大佬回滚版本。

这次的异常日志如下:

Data truncation: Incorrect datetime value: '0' for column 'clickTime' at row 1

Data truncation: Incorrect datetime value: '1' for column 'clickTime' at row 1

原来客户端还上报了数量庞大的 0 和 1。

当该字段长度不为10或13时,程序中是不做任何处理,直接插入到数据库的,数据库表结构中该字段为 datetime 类型的,所以当保存 0 或 1 时会报错。

那我们之前将 0 或 1, 转换后保存的究竟时什么呢? 再次通过 main 方法模拟一下:

public static void main(String[] args) {
    System.out.println(dateTimeMillisToString(0 * 1000, "yyyy-MM-dd HH:mm:ss"));
    System.out.println(dateTimeMillisToString(1 * 1000, "yyyy-MM-dd HH:mm:ss"));
    // 9位长度的时间戳
    System.out.println(dateTimeMillisToString(163826395 * 1000, "yyyy-MM-dd HH:mm:ss"));
}

image.png

发现,原来 SimpleDateFormatformat() 方法会对所有数字类型都进行格式化,这一点大家一定要注意了。

这次是真的解决了

把代码继续兼容优化:

String time = user.getClickTime();
if (StringUtils.isNotBlank(time)) {
    // 大于10位长度,则不再将 时间*1000
    if (time.length() > 10) {
        time = dateTimeMillisToString(time, YYYYMMDD_HHMMSS);
    } else {
        time = dateTimeMillisToString(time * 1000, YYYYMMDD_HHMMSS);
    }
}

同时和客户端的同事沟通,统一时间单位。