前言

在上下游数据对账时,发现存入数据库的时间跟发送给下游的时间上有差异,
比如上游存入 MySQL 数据库的时间为:2023-03-01 00:00:00,(数据库的字段类型为 datetime)
而下游接收到的时间为 2023-02-28 00:00:00. 执行顺序像下图这样:

2023-03-29T08:08:51.png

伪代码如下:

// new 一个时间
Date createTime = new Date();

// insert 入库
Model model = new Model();
model.setCreateTime(createTime);
modelMapper.insert(model);

// 构造消息体,向下游发送消息
Map<String, Object> message = new HashMap<>();
message.put("bizMessage", "something");
message.put("model", model);

// 将消息体转换成字符串向下游发送消息
sender.send(JsonUtil.beanToJson(message));

下面是beantToJson的实现,可以看出采用 jackson来实现,并且其中的date格式化字符串采用的是
SimpleDateFormat, 而且格式是 yyyy-MM-dd HH:mm:ss, 到秒.

public class JsonUtil {
    private static ObjectMapper mapper;

    static {
        mapper = new ObjectMapper();
        mapper.setLocale(Locale.CHINA);
       
        SimpleDateFormat smt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 设置默认时区
        mapper.setTimeZone(TimeZone.getDefault());
        // 时间类型格式化
        mapper.setDateFormat(smt);
    }

    public static <T> String beanToJson(T obj) {
        if (Objects.isNull(obj)) {
            return null;
        }
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new JsonSerializedException(e, "Bean转Json异常");
        }
    }
}

原因分析

先给出结果,然后我们再来证明

  • Mysql datetime 类型如果没有指定精度的话,会将秒后面的毫秒等进行四舍五入。
    比如:
    2023-02-28 23:59:59.677 会四舍五入成 2023-03-01 00:00:00
    2023-02-28 23:59:59.200 会四舍五入成 2023-02-28 00:00:00
    _
  • SimpleDateFormat 将 date 类型进行字符串格式化时,是使用 Calendar 来处理的,不会进行四舍五入的操作

接下来我们来用下面的程序测试一下数据库的四舍五入及date字符串格式化

Mysql datetime 类型如果没有指定精度的话,会将秒后面的毫秒等进行四舍五入。
比如:
2023-02-28 23:59:59.677 会四舍五入成 2023-03-01 00:00:00
2023-02-28 23:59:59.200 会四舍五入成 2023-02-28 00:00:00

SimpleDateFormat 将 date 类型进行字符串格式化时,是使用 Calendar 来处理的,不会进行四舍五入的操作。

由于Calendar 存在线程安全问题,所以需要用 Java 8 提供的java.time包下的 API 来替换 Calendar.

LocalDateTime now = LocalDateTime.now();
now = now.withNano(0);
Date nowDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
或者直接使用 joda 的 LocalDateTime (org.joda.time).

LocalDateTime now = new LocalDateTime();
now = now.withMillisOfSecond(0);
Date nowDate = now.toDate();

@Test
public void Test() {
    ModelMapper modelMapper = new ModelMapper();
    Sender sender = new Sender();

    // 创建一个 calender 对象,来构造确定时分秒及毫秒的时间
    // 2023-04-01 00:00:00.677
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(new Date());
    calendar.set(Calendar.MONTH, 3);
    calendar.set(Calendar.DATE, 1);
    calendar.set(Calendar.MINUTE,0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 677);

    Date createTime = calendar.getTime();

    // 构造 model
    Model model = new Model();
    model.setCreateTime(createTime);

    // insert model
    modelMapper.insert(model);

    // 构造消息,向下游发送消息
    Map<String, Object> message1 = new HashMap<>();
    message1.put("bizMessage", "message1");
    message1.put("model", model);

    // 将消息体转换成字符串向下游发送消息
    sender.send(JsonUtil.beanToJson(message1));

    // 更改calendar毫秒为 200
    // 2023-04-01 00:00:00.677
    calendar.set(Calendar.MILLISECOND, 200);
    createTime = calendar.getTime();

    // 更新 model 的时间
    model.setCreateTime(createTime);
    // insert model
    modelMapper.insert(model);

    // 构造消息,向下游发送消息
    Map<String, Object> message2 = new HashMap<>();
    message2.put("bizMessage", "message2");
    message2.put("model", model);

    // 将消息体转换成字符串向下游发送消息
    sender.send(JsonUtil.beanToJson(message2));
}

在 sender.send() 方法中对消息进行打印,运行上面的代码后,控制台输出如下:

{"bizMessage":"message1","model":{"createTime":"2023-04-01 22:00:00"}}
{"bizMessage":"message2","model":{"createTime":"2023-04-01 22:00:00"}}

mysql 数据库中的 create_time 字段两条分别为:

2023-03-17T15:11:11.png

可以看到 date 转字符串的结果是相同的,而数据库中对毫秒部分进行了四舍五入到秒.

MySQL的DATETIME类型

MySQL 官方对 DATETIME 类型的精度作了详细的说明 (DATE、TIMESTAMP类型的精度定义也一样),详情可参见官方文档

我总结了下,一共有以下几点:

  • DATETIME 可以指定精度(秒的后面还有几位),从 0-6,也就是最大精度可以到微妙级别(6位). 指定精度的方式为 datetime(6).
  • 如果不指定精度,默认为0,也就是秒级精度
  • 如果给的值的精度比字段设定的精度要高,则采取四舍五入(rounding),
  • 如果不想四舍五入,要直接舍弃掉后面的位,则需要开启 sqlmodel: TIME_TRUNCATE_FRACTIONAL.

对于第三点,官法的例子很清晰,我摘录了过来:

CREATE TABLE fractest( c1 TIME(2), c2 DATETIME(2), c3 TIMESTAMP(2) );
INSERT INTO fractest VALUES
('17:51:04.777', '2018-09-08 17:51:04.777', '2018-09-08 17:51:04.777');
mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1          | c2                     | c3                     |
+-------------+------------------------+------------------------+
| 17:51:04.78 | 2018-09-08 17:51:04.78 | 2018-09-08 17:51:04.78 |
+-------------+------------------------+------------------------+

需要开启 sqlmodel: TIME_TRUNCATE_FRACTIONAL 的方法为:

SET @@sql_mode = sys.list_add(@@sql_mode, 'TIME_TRUNCATE_FRACTIONAL');

开启后:

mysql> SELECT * FROM fractest;
+-------------+------------------------+------------------------+
| c1          | c2                     | c3                     |
+-------------+------------------------+------------------------+
| 17:51:04.77 | 2018-09-08 17:51:04.77 | 2018-09-08 17:51:04.77 |
+-------------+------------------------+------------------------+
文章目录