浅谈MySQL timestamp(3)问题
作者:johnny233 发布时间:2024-01-14 17:47:23
背景
最近在负责开发维护的一款数据平台,有一个功能是把数据从某个源头数据源(如常规的JDBC数据源,MySQL,Oracle等)推到目地数据源(还包括企微,MQ等)。一次推送数据就是一个任务,当然需要记录此次推送任务的执行情况,如任务的开始时间,结束时间,任务名称,任务执行状态,任务日志(失败原因),执行人,执行方式(手动执行还是定时触发)。另外,从来源数据源取数,怎么取数是通过SQL指定的,那我们还可以记录一下SQL查询耗时,以及SQL查询条数。总耗时就是任务执行结束时间减去任务开始时间,总耗时肯定大于SQL查询耗时。
统计SQL查询耗时时,还需要考虑到SQL取数的数据量,假如SQL查询量为1000w,程序不可能一次查询全部数据,然后加工处理并发送到下游目地数据源,故而需要设置分批。假设批次为50w,则SQL查询耗时为20次查询的耗时之和。
扯远一句,项目是接手维护的,一开始的设计开发者是用秒来记录SQL查询耗时,这样一看就不严谨,因为很多SQL查询耗时根本不需要1秒。事实上就算查询耗时超过1s,1.6s和1.7s也是有区别的,故而需要带毫秒来优化记录SQL查询耗时。最后在前端展示时,用小数点来表示毫秒。
很常规,看起来也没有任何问题的数据表设计:
create table execlog (
id bigint(11) auto_increment primary key,
total_sql_time bigint null comment 'SQL执行耗时,单位豪秒',
start_date timestamp default CURRENT_TIMESTAMP not null comment '执行开始时间',
end_date timestamp null comment '执行结束时间',
);
注:total_sql_time
注释单位毫秒,以及程序记录单位是后来优化调整的。
某次任务执行记录截图如下,发现总耗时为0秒,而查询耗时为146毫秒。这显然不符合逻辑。
优化
优化思路也不难,就是任务开始时间需要记录到毫秒级别,改进后的表结构为:
create table execlog (
id bigint(11) auto_increment primary key,
total_sql_time bigint null comment 'SQL执行耗时,单位豪秒',
start_date timestamp(3) default CURRENT_TIMESTAMP(3) not null comment '执行开始时间',
end_date timestamp(3) null comment '执行结束时间',
);
值得注意的是,MySQL直到版本5.6(不太确定)才支持,如何知道自己使用的MySQL Server版本是否支持timestamp(3)
,执行语句即可验证,没有报错并且返回毫秒数表示支持:select now(3);
优化上面截图中的日志记录问题分为两个步骤,即日志记录和日志显示。
日志记录
增加一个取当时时间精确到毫秒的静态方法:
public static final String COMMON_DATE_WITH_MILLI_SECOND = "yyyy-MM-dd HH:mm:ss:sss";
public static String getNowWithMilliSecond() {
SimpleDateFormat sdf = new SimpleDateFormat(Constant.COMMON_DATE_WITH_MILLI_SECOND);
return sdf.format(new Date());
}
表结构如上改进后,发现start_date
记录没有问题,带3位小数点。但end_date
记录不到小数点,即未记录到毫秒,于是怀疑表结构不对。
某次任务执行肯定会有start_date
,故而设置为not null
,但会因发布,或调试中断等各种原因导致记录不到end_date
时间点,因此字段不能设置为not null
。
落不到数据,怀疑需要给一个默认值,于是如下改表结构
create table execlog (
id bigint(11) auto_increment primary key,
start_date timestamp(3) default CURRENT_TIMESTAMP(3) not null comment '执行开始时间',
end_date timestamp(3) default CURRENT_TIMESTAMP(3) null comment '执行结束时间'
);
但是还是记录不到结束时间的毫秒数。
此时只能好好看代码,end_date
是在任务结束(不管是正常结束还是异常结束,异常结束在finally语句块)时updateExecLog
更新:
<update id="updateExecLog" parameterType="com.xy.cloudiview.common.po.ExecLog">
UPDATE execlog t
<set>
<if test="errorLog != null">
t.error_log = #{errorLog},
</if>
t.end_date = now(),
</set>
WHERE t.id = #{id}
</update>
此处已经指定end_date
取数为now()
,肯定不会记录到毫秒数的。需改成t.end_date = now(3),
。
再看另外一个updateExecLog
更新语句:
<update id="updateByPrimaryKeySelective" parameterType="com.xy.cloudiview.common.po.ExecLogWithBlobs">
update execlog
<set>
<if test="endDate != null">
end_date = #{endDate,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{id,jdbcType=BIGINT}
</update>
貌似找到一点问题解决思路,开启MySQL日志打印功能:
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
一个很无厘头的尝试:
<if test="endDate != null"> end_date = #{endDate,jdbcType=TIMESTAMP(3)},</if>
报错:
MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error resolving JdbcType. Cause: java.lang.IllegalArgumentException: No enum constant org.apache.ibatis.type.JdbcType.TIMESTAMP(3)
at java.lang.Enum.valueOf(Enum.java:238)
那就把jdbcType
去掉:
<if test="endDate != null">
end_date = #{endDate},
</if>
打印日志如下:
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7ef24deb] will not be managed by Spring
==> Preparing: update execlog SET end_date = ? where id = ?
==> Parameters: 2022-10-12 17:08:13:013(String), 31542157(Long)
nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Incorrect datetime value: '2022-10-12 16:05:47:047' for column 'end_date' at row 1
也就是说,当MyBatis遇到MySQL timestamp(3)
时,MyBatis上面这种写法支持不了。
于是只能换一种写法:
<update id="updateByPrimaryKeySelective" parameterType="com.xy.cloudiview.common.po.ExecLogWithBlobs">
update execlog
<set>
<if test="modelId != null">
model_id = #{modelId,jdbcType=BIGINT},
</if>
end_date = now(3),
</set>
where id = #{id,jdbcType=BIGINT}
</update>
解决问题。也不需要在Java代码层设置带毫秒数的当前时间。
此时,回头看看第一个截图,里面有一个日志类型的字段,文章开头没有描述。这里解释一下,因为平台有多种类型的任务,涉及多个maven Module模块和多个表,于是有多个execlog的insert和update语句。
再来看一个insertExecLog语句,省略无关字段,这种方法是MyBatis插件generator自动生成的,也没有任何问题,就是看起来非常冗余,两个<trim>
语句块,再加上每个<trim>
语句块里面每个字段都有<if>
语句块条件判断语句:
<insert id="insertSelective" parameterType="com.xy.cloudiview.common.po.ExecLogWithBlobs">
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into execlog
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="startDate != null">
start_date,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="startDate != null">
#{startDate,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
维护项目的一个默认的潜规则,就是除非没有大的问题,尽可能不要大面积改动代码,大面积改动还不如重构,重构的前提是对项目非常了解。
当然这个地方只是一个MyBatis方法而已,还达不到重构那个深度。但是我也是想着尽可能不要改动太多,于是改动如下:
<insert id="insertSelective" parameterType="com.xy.cloudiview.common.po.ExecLogWithBlobs">
<selectKey resultType="java.lang.Long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into execlog
<trim prefix="(" suffix=")" suffixOverrides=",">
start_date,
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
now(),
</trim>
</insert>
报错信息就不贴出来了,原因还是当MyBatis遇到MySQL timestamp(3)
时,MyBatis上面这种写法不支持。
改动方法:
删除start_date
对应的if
判断语句块,让MySQL的default CURRENT_TIMESTAMP(3)
来使其生效
使用如下方法:
<insert id="saveExecLog" parameterType="com.xy.cloudiview.common.po.ExecLogWithBlobs">
INSERT INTO execlog(start_date) VALUES (now(3))
</insert>
日志查询
改进前的执行日志列表页查询语句为:
select
DATE_FORMAT(t.start_date,'%Y-%m-%d %H:%i:%s') as startDate,
TO_SECONDS(t.end_date)-TO_SECONDS(t.start_date) as totalTime
from execlog
改进后的:
select
SUBSTRING(DATE_FORMAT(t.end_date, '%Y-%m-%d %H:%i:%s.%f'), 1, 23) AS endDate,
TIMESTAMPDIFF(MICROSECOND, t.start_date, t.end_date) / (1000 * 1000) AS totalTime
from execlog
最后实现的效果是:
参考
get-milliseconds-with-date-format-in-mysql
https://stackoverflow.com/questions/26299149/timestamp-with-a-millisecond-precision-how-to-save-them-in-mysql
https://stackoverflow.com/questions/20520443/mysql-timestamp-to-default-null-not-current-timestamp
来源:https://blog.csdn.net/lonelymanontheway/article/details/127292085


猜你喜欢
- 如 现有字符串 "[]aseabcd[12345]ddxabcdsx[]",要截取"abcd[" 和
- 选择排序选择排序是一种简单的比较排序算法,它的算法思路是首先从数组中寻找最小(大)的元素,然后放到数组中的第一位,接下来继续从未排序的元素中
- 目录sync.Cond 可以用来干什么?与 Sync.Mutex 的区别sync.Cond 使用场景sync.Condsync.Cond 有
- 作者的BLOG:http://www.planabc.net/地图弹窗(map pop)具体演示运行代码框<!DOCTYPE html
- 1 什么是嵌套循环所谓嵌套循环就是一个外循环的主体部分是一个内循环。内循环或外循环可以是任何类型,例如 while 循环或 for 循环。
- 本文针对Python的全局变量实现方法简述如下:先来看下面一段测试程序:count = 0def Fuc(count): pri
- 用法:DataFrame.drop(labels=None,axis=0, index=None, columns=None, inplac
- 当用GOOGLE查的时候,内容几乎都是一样的。但是你想要的东西,一个也没有。例如,我就找不到中国人写的如何使用PYTHON来创建一个XML文
- 前几天,酋长同学在日志里提到了关于Google宽松的管理制度,一个产品任务下来是没有时间限制的,Google深信在有时间限制下的产品肯定是不
- I. 前言在前面的一篇文章PyTorch搭建LSTM实现时间序列预测(负荷预测)中,我们利用LSTM实现了负荷预测,但我们只是简单利用负荷预
- 在sql语句中,如果查找某个文本字段值为空的可以用select * from 表 where 字段=''但是如果
- 前言最近在学习过程中需要用到pytorch框架,简单学习了一下,写了一个简单的案例,记录一下pytorch中搭建一个识别网络基础的东西。对应
- 一、什么是集合二、集合的创建方式集合中的元素不能重复#地点:湖北武汉#姓名:学工科的皮皮志#开发时间:2022/2/27 19:
- Step 1. 获取混淆矩阵#首先定义一个 分类数*分类数 的空混淆矩阵 conf_matrix = torch.zeros(Emotion
- 场景说明假设有一个mysql表被水平切分,分散到多个host中,每个host拥有n个切分表。 如果需要并发去访问这些表,快速得到查询结果,
- 前言我第一次见到飞机大战是在我小学五年级下半学期的时候(2020年),这个游戏中可以说包含了几乎所有我目前可接触到的pygame知识。一、p
- 起步Django 是个同步框架,本文并不是 让 Django 变成异步框架。而是对于在一个 view 中需要请求多次 http api 的场
- SQL Server日期计算 通常,你需要获得当前日期和计算一些其他的
- 目录一,猫狗数据集数目构成二,数据导入三,数据集构建四,模型搭建五,模型训练六,模型测试作为tensorflow初学的大三学生,本次课程作业
- 描述log10() 方法返回以10为基数的x对数,x>0。语法以下是 log10() 方法的语法:import mathmath.lo