项目中一个 1 毫秒引发的问题
问题描述:
1. 项目分为两个部分,一个后台的数据采集程序,接收来自各个传感器发送过来的数据,解码后以特定的格式存储在数据库对应的表中,另一个是web部分,用户可以通过web查看这些数据的情况(历史数据和实时数据);
2. 在存储检测数据的表MonitorData中,以设备ID(区分不同的物理设备)、类型属性ID(同一个设备可以检测多种数据,比如风速风向传感器就能够检测出风速和风向两种数据,这两种属性就通过类型属性ID来区分)、时间戳(记录后台收到数据的时间)来唯一标识一条数据;
3. 在web部分,有一个模块可以通过指定起始时间段来查询历史数据;
4. 好了现在问题来了,现在我通过这个模块查看一段时间内的历史数据,查询结果如下:
咦,为什么会出现这么多0值呢?来看一下数据库中的数据库(select * from MonitorData):
在数据库中对应的值根本不是0,而web端读到的值居然是0,这是为什么?
WHY?
带着这个问题,首先找到了web端对应的页面部分:
对,就是这个listMonitorData.jsp,让我们来看一下它的源码,在源码中我们主要是要找到在后台这个数据请求是交给那一个类的方法处理的,所以我们找到它的表单提交路径:
该web使用了struts2框架,我们看到,该表单提交给了一个叫listMonitorDataDevice的action来处理,那我们继续去找该action
直觉告诉我,我要的action很有可能在struts-device.xml中,因为我们查询的设备的数据嘛。打开一看,果然有对应的action:
看到class="com.water.struts2.action.device.DeviceAction" method="{1}",我们知道接下来调用的方法是com.water.struts2.action.device.DeviceAction类中的listMonitorData方法。对method参数不熟的同学可以参考一下这篇博客。
好了,那让我们直接找到对应的方法中的抓取数据部分吧
到这里我们抓取了数据库中这段时间的全部数据的时间戳,下面是读取数据的主要代码,也是出问题的部分:
1 MonitorDataVo mvo = new MonitorDataVo(); 2 if (list != null && list.size() > 0) { 3 for (int m = 0; m < list.size(); m++) { 4 String datatimestamp = StringUtil 5 .getStringValue((list.get(m))); 6 // System.out.println("+++++++datatimestamp: " + datatimestamp); 7 for (int i = 0; i < attributes.size(); i++) { 8 DeviceAttributeVo attriVo = attributes.get(i); 9 // 首先构建ID 10 MonitorDataId id = new MonitorDataId(); 11 id.setDevice(device); 12 DeviceTypeAttribute attr = new DeviceTypeAttribute(); 13 attr.setDeviceTypeAttributeId(attriVo.getDeviceTypeAttributeId()); 14 id.setDeviceTypeAttribute(attr); 15 Date tmpDate = DateTimeUtil.parseDateTimeHMSs(datatimestamp); 16 // System.out.println("=========tmpdate: " + tmpDate.getTime()%1000); 17 id.setDataTimestamp(tmpDate); 18 // 新建 19 MonitorData mdata = new MonitorDataDAO().findById(id); 20 if (mdata == null) { 21 System.out.println("==NULL"); 22 mdata = new MonitorData(); 23 mdata.setDataValue(0.0); 24 } 25 mvo.addMonitorData(mdata); 26 } 27 mvo.setDatatimestamp(DateTimeUtil 28 .parseDateTimeHMSs(datatimestamp)); 29 mvolist.add(mvo); 30 mvo = new MonitorDataVo(); 31 } 32 }
直观上来看,根据上一步得到的时间戳的list,知道了数据的条数,对于每条记录我们用Hibernate的get方法根据ID查询出对应的value(在findById方法中调用Hibernate的get方法)
MonitorData instance = (MonitorData) getSession().get("com.water.hibernate.MonitorData", id);
这里我们用了一个MonitorData的类来表示一个数据。attributes属性是一个action类中的全局数据,表示当前设备具有的属性,如风速风向。到这里我们初步明白了为什么有些web页面上有些值是0了,是因为get方法没有根据ID匹配到对象,所以返回了null,继而findById返回了null,继而mdata的value值为0.0,但是为什么get方法会查询不到对应的对象呢?我们所使用的ID是这样的:
private Device device; private DeviceTypeAttribute deviceTypeAttribute; private Date dataTimestamp;
它包含了三个成员变量,device是根据deviceID来查询到的
Device device = new DeviceDAO().findById(deviceId);
而deviceId是这个action类的String型的成员变量,它的赋值是通过反射找到set方法实现的,而deviceTypeAttribute是根据attributes来的,attributes是根据deviceID查询数据库得到的,deviceID同样是这个action类的String型的成员变量,因此这两个变量应该都没有什么问题。datatimestamp是根据前面的时间戳list得到的String,再转化成Date类型而得到的,按理说也没什么问题。
Date tmpDate = DateTimeUtil.parseDateTimeHMSs(datatimestamp);
从现象归纳问题的特点,从而发现问题的根源
通过查看源码找问题貌似遇到了点问题,于是我们回过头来再去看看问题本身。首先我们观察是具体哪些时间出现了问题,在数据库中对应时间点是不是有什么特征:
我们发现,在数据库中的时间戳是精确到毫秒的,而出现问题的时间点(也就是通过ByID的get方法找不到的记录)有一个共同的特点,那就是毫秒位为0,或者说按正常书写小数点后末尾的0是可以省去的,也就是达不到三位,比如14.890,可以写成14.89。
根据问题特点猜测引起问题的可能原因:
出现问题主要是因为findById(id)这个函数不能得到id对应的对象,进一步来说是Hibernate中的get方法返回了null,而get方法基于的ID中包含了一个datatimestamp的Date对象,凡是datatimestamp中毫秒位为0的ID都无法正常get到数据。而Hibernate的get方法最终会转化成一条SQL语句对数据库进行查询(当然首先是在Session中查找)
Hibernate: select monitordat0_.DeviceID as DeviceID10_0_, monitordat0_.DeviceTypeAttributeID as DeviceTy2_10_0_, monitordat0_.DataTimestamp as DataTime3_10_0_, monitordat0_.DataUpdateTime as DataUpda4_10_0_, monitordat0_.DataValue as DataValue10_0_ from water.dbo.MonitorData monitordat0_ where monitordat0_.DeviceID=? and monitordat0_.DeviceTypeAttributeID=? and monitordat0_.DataTimestamp= ?
所以猜测:
SQL语句在做查询的时候条件对比是如何实现的?尤其是datetime类型的数据对比,最终是转化成格式化的时间字符串进行对比的,还是转化成毫秒数进行对比,根据问题的特征,有可能最终是转化成格式化时间的字符串进行的对比,也就是说数据库中的时间保留了毫秒位最后的‘0’,而java类中的Date类型最终是简化了写法,把最后的‘0’省去了,因此两者出现了不一致,所以该条记录查询不到。
临时解决方案:
- 由于采集的传感器数据是每个几秒钟发送一次,所以毫秒级别的误差根本就不痛不痒,因此在数据写入的时候做点“手脚”不是皆大欢喜吗。
- 既然问题出现的时间点是毫秒位为0的数据,那我们主要让其毫秒位不出现0不就可以了吗。
- 需要注意的一点是:sqlserver中所有的datetime类型的值在显示、处理时有所调整。即会圆整到几个特殊的毫秒个位值:0、3、7:如:(9、0、1) 会引起进0调整;(5、6、7、8)引起7调整;(2、3、4)引起3调整。因此我们在做手脚的时候不能值偏移一个毫秒,因此我们偏移三毫秒。
Date dt = new Date(); System.out.println("当前时间" + dt); String tmpDate = String.valueOf(dt.getTime()%10); long tmpDt = dt.getTime(); if(tmpDate.equalsIgnoreCase("0") || tmpDate.equalsIgnoreCase("9") || tmpDate.equalsIgnoreCase("1")) { tmpDt += 3; } Date newDate = new Date(tmpDt); id.setDataTimestamp(newDate);