近期团队的个别项目在进行框架升级后,部分时间值存在8小时偏差,缘由是错误的将数据库中的时间数据理解成了UTC时间(旧版本认为是北京时间) java
考虑到将来项目对于时间理解的一致性,我决定将项目统一为使用UTC时间,经调研,造成本文 mysql
mysql数据库拥有时区设置,默认使用系统时区 sql
可经过以下语句查询当前时区 数据库
show variables like '%time_zone%'; json
下图为我我的机器上mysql数据库时区设置: app
项目线上数据库时区设置以下: 框架
可见数据库使用系统时间CST——China Standard Time UTC+8:00 中国沿海时间(北京时间) ide
datetime 函数
实际格式储存(Just stores what you have stored and retrieves the same thing which you have stored.) this
与时区无关(It has nothing to deal with the TIMEZONE and Conversion.)
timestamp
值以UTC毫秒数保存( it stores the number of milliseconds)
存储及检索时根据当前时区设置,对时间数值作转换
因为timestamp与时区相关,且线上数据库时区设置为北京时间(即UTC+8:00)。所以,当数据库中使用了timestamp列,若使用不当,统一UTC格式时间改造将极可能会引入错误! 后面详述理由
项目新框架中经过UTCTimeZoneConfiguration类型,在项目初始化时设置当前进程的默认时区
@Configuration public class UTCTimeZoneConfiguration implements ServletContextListener{ public void contextInitialized(ServletContextEvent event) { System.setProperty("user.timezone", "UTC"); TimeZone.setDefault(TimeZone.getTimeZone("UTC")); } public void contextDestroyed(ServletContextEvent event) {} }
日期时间类型可使用 java.util.Date,但推荐使用更为方便的joda DateTime,本节介绍joda DateTime 序列化/反序列化使用方式
Joda DateTime 类型用于定义接口输入输出参数,需进行序列化/反序列化操做。与原生的Date类型不一样,DateTime须要作一点额外处理
1、Model类型的日期字段使用类型DateTime替代Date
实例代码以下
public class Entity { @JsonSerialize(using = UTCDateTimeSerializer.class) @JsonDeserialize(using = UTCDateTimeDeserializer.class) private DateTime dateTime; public DateTime getDateTime() { return dateTime; } public void setDateTime(DateTime dateTime) { this.dateTime = dateTime; } }
其中UTCDateTimeSerializer与UTCDateTimeDeserializer类的实现见附录
2、Get请求接受时间参数
此时,一种有效的处理方式是使用字符串接受日期参数,以下:
@RequestMapping(value = "/xxx", method = RequestMethod.GET) public CommonResponse getXxx(@RequestParam(value = "beginTime") String beginTimeText, @RequestParam(value = "endTime") String endTimeText) { DateTime beginTime = DateTime.parse(beginTimeText).withZone(DateTimeZone.UTC); DateTime endTime = DateTime.parse(endTimeText).withZone(DateTimeZone.UTC); ... }
以Joda DateTime类型举例说明使用方法,某Dao类型中存在的两个方法以下:
public void update(int id, DateTime dateTime) { String sql = "UPDATE " + TABLE_NAME + " SET datetime = ? WHERE id = ?"; jdbcTemplate.update(sql, new Timestamp(dateTime.getMillis()), id); } public DateTime getDateTime(int id) { String sql = "SELECT datetime FROM " + TABLE_NAME + " WHERE id = ?"; List<DateTime> dateTimeList = jdbcTemplate.query(sql, new Object[] {id}, new RowMapper<DateTime>() { @Override public DateTime mapRow(ResultSet rs, int rowNum) throws SQLException { return new DateTime(rs.getTimestamp("datetime").getTime()); } }); return dateTimeList.size() > 0 ? dateTimeList.get(0) : null; }
插入或更新数据,传递的时间参数请使用 new Timestamp(dateTime.getMillis())
读取时间参数,使用new DateTime(rs.getTimestamp("datetime").getTime())
数据库timestamp类型适合用来记录数据的最后修改时间
其余场景建议使用datetime或者int
方案一 更改会话时区为UTC时间
对timestamp列的操做与datetime列的操做不作区分,此时须要设置数据链接会话的时区,默认为北京时间,须要设置为UTC时间,经过以下语句设置
set time_zone = '+0:00';
实际项目中使用数据库链接池,建立datasource后使用以下方式设置时区,将对全部链接生效
dataSource.setInitSQL("set time_zone = '+0:00'");
经此操做后,时区统一为UTC时间,Dao中时间操做,无需对timestamp作特殊处理
方案二 不更改会话时区
因为不更改时区,timestamp类型数据的使用存在必定限制
一、 如何更新timestamp数据
对于数据库表中的timestamp列,其值的更新应当由数据库自行维护,在create table时设置,以下:
CREATE TABLE t1 ( ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
可简写以下
CREATE TABLE t1 ( ts TIMESTAMP );
不容许程序自主更新timstamp列数据
线上数据库时区为北京时间,其接受到的日期数据被视为北京时间,而上层程序业务逻辑统一使用UTC时间,时区不统一。所以避免数据库记录的日期数据理解不一致,不容许程序经过写操做sql语句更新timestamp列
下图数据为本人实测数据,timestamp列由程序进行更新,update_time列则由数据库自动更新
前者显示的是UTC时间,看似合理,实则错误,数据库内部存储时间为UTC-8:00
update_time符合数据库时区设置,返回北京时间,内部实际存储UTC时间
二、 如何读取timestamp数据
为避免从数据库中获取时区相关时间(北京时间),强制使用UTC时间,使用函数UNIX_TIMESTAMP获取1970年至今秒数,转换成DateTime时乘以1000转变为毫秒
public DateTime getTimestamp(int id) { String sql = "SELECT UNIX_TIMESTAMP(update_time) as unix_timestamp FROM " + TABLE_NAME + " WHERE id = ?"; List<DateTime> dateTimeList = jdbcTemplate.query(sql, new Object[] {id}, new RowMapper<DateTime>() { @Override public DateTime mapRow(ResultSet rs, int rowNum) throws SQLException { return new DateTime(rs.getLong("unix_timestamp") * 1000); } }); return dateTimeList.size() > 0 ? dateTimeList.get(0) : null; }
设置全局时区,须要管理员权限
使用本机系统时区
SET GLOBAL time_zone = SYSTEM;
使用UTC时间
SET GLOBAL time_zone = '+0:00';
使用北京时间
SET GLOBAL time_zone = '+8:00';
设置当前链接会话时区
set time_zone = '+0:00';
UTCDateTimeSerializer 完成DateTime对象到UTC时间字符串的转换,格式为:yyyy-MM-ddTHH:mm:ssZ
UTCDateTimeDeserializer 完成时间字符串到DateTime对象的转换,转换为UTC时区
具体实现以下:
public class UTCDateTimeSerializer extends JsonSerializer<DateTime> { @Override public void serialize(DateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException { String dateTimeAsString = dateTime.withZone(DateTimeZone.UTC).toString(BecConstant.DATETIME_FORMAT); jsonGenerator.writeString(dateTimeAsString); } } public class UTCDateTimeDeserializer extends JsonDeserializer<DateTime> { @Override public DateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonToken currentToken = jsonParser.getCurrentToken(); if (currentToken == JsonToken.VALUE_STRING) { String dateTimeAsString = jsonParser.getText().trim(); return DateTime.parse(dateTimeAsString).withZone(DateTimeZone.UTC); } return null; } }