灵异bug:神奇的23:59:59 变00:00:00
数据库中本来预期为 xxxx-xx-xx 23:59:59的字段竟有一部分是xxxx-xx-xx 00:00:00
背景描述:
最近在帮朋友的公司处理一些技术工作,项目中涉及一个优惠券活动,系统逻辑设置为优惠券的截至时间(endtime)为当前时间+7天后的最后一秒,如当前时间为2017-08-22 09:15:15,那么endtime应该是2017-08-29 23:59:59,但在数据库中发现了部分行为2017-08-30 00:00:00。项目使用java开发语言,采用了hibernate,数据为mysql 。 既然发现了这个问题,那咱就得想办法解决掉。
除错之旅一:
第一眼看到这个问题时, 因为不是自己写的代码,猜测肯定是代码写错了,出现了分支代码导致一部分数据正确一部分不正确。按照业务逻辑对代码进行了定位,经简化相关代码如下:
@Service
public class CouponServiceImpl implements CouponService{
.....
public void addCoupon(){
.........
coupon.setEndDate(lastMoment(getEndTime(3, new Date(), 7)));
couponDao.create(coupon);
}
public static Date lastMoment(Date date) {
Calendar cl = Calendar.getInstance();
cl.setTime(date);
cl.set(Calendar.HOUR_OF_DAY, 23);
cl.set(Calendar.MINUTE, 59);
cl.set(Calendar.SECOND, 59);
return cl.getTime();
}
}
仔细看了代码没有分支逻辑,只有这一处设置endtime的业务代码,看来代码本身好像没什么错误。
除错之旅二:
代码没有明显错误,那会不是jdk的问题,项目使用的jdk1.6。按照这个思路百度了jdk1.6的一些bug没有发现有价值的内容,所以决定看一下Calendar 和Date的源码。在Calendar源码中发现如下描述信息:
*Example: Consider a GregorianCalendar
originally set to August 31, 1999. Calling set(Calendar.MONTH,
Calendar.SEPTEMBER) sets the date to September 31,
- This is a temporary internal representation that resolves to
October 1, 1999 if getTime()is then called. However, a
call to set(Calendar.DAY_OF_MONTH, 30) before the call to
getTime() sets the date to September 30, 1999, since
no recomputation occurs after set() itself.
既然会存在9月31日的中间状态,那会不会秒或者毫秒也存在这种状态呢?比如业务代码在处理时获得的new Date()的毫秒正好处于1000的临界状态呢,那么在设置时分秒后,毫秒进位导致变成了00:00:00。按照这个假设写测试代码:
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
try {
Date now = new Date();
Calendar cl = Calendar.getInstance();
cl.setTime(now);
// cl.set(Calendar.MILLISECOND, 1000);
if (cl.get(Calendar.MILLISECOND) < 900) {
Thread.sleep(100);//将时间逼近900毫秒以上
continue;
}
System.out.print(cl.get(Calendar.MILLISECOND) + "::");
now = new Date();
System.out.print(now + " " + getEndTime(now, 7) + " ");
Date date = lastMoment(now);
Thread.sleep(1);
System.out.println(date);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
虽然毫秒逼近了999,但并没有出现神奇的结果,结果无一例外的否定这个思路。时分秒都不存在类似于日的临界状态,其实事后一想确实这样,毕竟时分秒的上限都是固定不变,只有日会随着月而出现上限不固定的情况,比如2月28日、29日,3月31日等。
除错之旅三:
不断的被否定才能更好的成长,一时没有思路就先好好吃饭睡觉第二天再思考(当然前提是生产环境已经处理掉问题了)。翌日也就是今天开始重新思考这个bug,既然代码层、jdk层没有问题,那会不会和数据库或者中间件有关呢?按照这个思路查看这个实体类的字段类型以及表字段的绑定类型和数据库ddl
@Temporal(TemporalType.TIMESTAMP)
private Date endDate;
endDate` datetime DEFAULT NULL,
忽然想到数据库的datetime可能会把毫秒做四舍五入处理,而ddl中默认datetime为0也就是不存放毫秒位,所以出现这种bug。还是以结果说话吧,创建表进行测试:
use test;
CREATE TABLE jamtest
(
id
INT (11) NOT NULL AUTO_INCREMENT,
date1
datetime NULL,
date2
datetime(2) NULL,
PRIMARY KEY (id
)
) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8 COLLATE = utf8_general_ci;
insert into jamtest(date1,date2) VALUES ('2017-01-01 23:59:59.551','2017-01-01 23:59:59.451');
SELECT * from jamtest;
date1字段自动进行了进位。
查询结果
果然问题复现了,既然复现了解决办法自然就好找了:
1、在设置时分秒时也设置毫秒
2、设置数据库字段的精度为3,保留所有毫秒值,避免进位
3、或者设置数据库不启用毫秒功能,慎用。
问题引申:
如果你在执行上一步的sql语句时报错了,那说明的mysql版本低不支持毫秒特性,而这个低版本的mysql是不会出现这个灵异bug的,所以保持生产环境和开发环境的一致对解决问题是有很大影响的。
这个问题还是代码或者表结构设计不合理造成。继续深入分析,在网上找到了一博文解读的很深入博客,主要是mysql-connector-java在5.1.6以后的版本中会根据服务端的特性进行毫秒的传递或者舍弃,而5.1.6以前是直接舍弃毫秒的。 mysql 5.6.4及以上版本的mysql server端支持fractional second part(fsp),但如果client提交过来的小数位数超过server端建表时指定的小数位数,mysql server会自动进行四舍五入的截断,没有任何警告或异常。