SpringBoot時間戳與MySql資料庫記錄相差14小時排錯
專案中遇到儲存的時間戳與真實時間相差14小時
的現象,以下為解決步驟.
問題
CREATE TABLE `incident` ( `id` int(11) NOT NULL AUTO_INCREMENT, `created_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `recovery_time` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4;
以上為資料庫建表語句,其中created_time
是插入記錄時自動設定,recovery_time
需要手動進行設定.
測試時發現,created_time
為正確的北京時間,然而recovery_time
則與設定時間相差14小時.
嘗試措施
jvm時區設定
//設定jvm預設時間
System.setProperty("user.timezone", "UTC");
資料庫時區查詢
檢視資料庫時區設定:
show variables like '%time_zone%'; --- 查詢結果如下所示: --- system_time_zone: CST --- time_zone:SYSTEM
查詢CST
發現其指代比較混亂,有四種含義(參考網址:https://juejin.im/post/5902e087da2f60005df05c3d):
- 美國中部時間 Central Standard Time (USA) UTC-06:00
- 澳大利亞中部時間 Central Standard Time (Australia) UTC+09:30
- 中國標準時 China Standard Time UTC+08:00
- 古巴標準時 Cuba Standard Time UTC-04:00
此處發現如果按照美國中部時間
進行推算,相差14小時
,與Bug吻合.
驗證過程
MyBatis轉換
程式碼中,時間戳使用Instant
package org.apache.ibatis.type
下的InstantTypeHandler
.
@UsesJava8
public class InstantTypeHandler extends BaseTypeHandler<Instant> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Instant parameter, JdbcType jdbcType) throws SQLException {
ps.setTimestamp(i, Timestamp.from(parameter));
}
//...程式碼shenglve
}
除錯時發現parameter
為正確的UTC
時.
函式中呼叫Timestamp.from
將Instant
轉換為Timestamp
例項,檢查無誤.
/**
* Sets the designated parameter to the given <code>java.sql.Timestamp</code> value.
* The driver
* converts this to an SQL <code>TIMESTAMP</code> value when it sends it to the
* database.
*
* @param parameterIndex the first parameter is 1, the second is 2, ...
* @param x the parameter value
* @exception SQLException if parameterIndex does not correspond to a parameter
* marker in the SQL statement; if a database access error occurs or
* this method is called on a closed <code>PreparedStatement</code> */
void setTimestamp(int parameterIndex, java.sql.Timestamp x)
throws SQLException;
繼續跟蹤setTimestamp
介面,其具體解釋見程式碼註釋.
Sql Driver轉換
專案使用com.mysql.cj.jdbc
驅動,跟蹤其setTimestamp
在ClientPreparedStatement
類下的具體實現(PreparedStatementWrapper
類下實現未進入).
@Override
public void setTimestamp(int parameterIndex, Timestamp x) throws java.sql.SQLException {
synchronized (checkClosed().getConnectionMutex()) {
((PreparedQuery<?>) this.query).getQueryBindings().setTimestamp(getCoreParameterIndex(parameterIndex), x);
}
}
繼續跟蹤上端程式碼中的getQueryBindings().setTimestamp()
實現(com.mysql.cj.ClientPreparedQueryBindings
).
@Override
public void setTimestamp(int parameterIndex, Timestamp x, Calendar targetCalendar, int fractionalLength) {
if (x == null) {
setNull(parameterIndex);
} else {
x = (Timestamp) x.clone();
if (!this.session.getServerSession().getCapabilities().serverSupportsFracSecs()
|| !this.sendFractionalSeconds.getValue() && fractionalLength == 0) {
x = TimeUtil.truncateFractionalSeconds(x);
}
if (fractionalLength < 0) {
// default to 6 fractional positions
fractionalLength = 6;
}
x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.session.getServerSession().isServerTruncatesFracSecs());
//注意此處時區轉換
this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", targetCalendar,
targetCalendar != null ? null : this.session.getServerSession().getDefaultTimeZone());
StringBuffer buf = new StringBuffer();
buf.append(this.tsdf.format(x));
if (this.session.getServerSession().getCapabilities().serverSupportsFracSecs()) {
buf.append('.');
buf.append(TimeUtil.formatNanos(x.getNanos(), 6));
}
buf.append('\'');
setValue(parameterIndex, buf.toString(), MysqlType.TIMESTAMP);
}
}
注意此處時區轉換,會呼叫如下語句獲取預設時區:
this.session.getServerSession().getDefaultTimeZone()
獲取TimeZone
資料,具體如下圖所示:
檢查TimeZone
類中offset
含義,具體如下所示:
/**
* Gets the time zone offset, for current date, modified in case of
* daylight savings. This is the offset to add to UTC to get local time.
* <p>
* This method returns a historically correct offset if an
* underlying <code>TimeZone</code> implementation subclass
* supports historical Daylight Saving Time schedule and GMT
* offset changes.
*
* @param era the era of the given date.
* @param year the year in the given date.
* @param month the month in the given date.
* Month is 0-based. e.g., 0 for January.
* @param day the day-in-month of the given date.
* @param dayOfWeek the day-of-week of the given date.
* @param milliseconds the milliseconds in day in <em>standard</em>
* local time.
*
* @return the offset in milliseconds to add to GMT to get local time.
*
* @see Calendar#ZONE_OFFSET
* @see Calendar#DST_OFFSET
*/
public abstract int getOffset(int era, int year, int month, int day,
int dayOfWeek, int milliseconds);
offset
表示本地時間
與UTC
時的時間間隔(ms)
.
計算數值offset
,發現其表示美國中部時間
,即UTC-06:00
.
Driver
推斷Session
時區為UTC-6
;Driver
將Timestamp
轉換為UTC-6
的String
;MySql
認為Session
時區在UTC+8
,將String
轉換為UTC+8
.
因此,最終結果相差14小時,bug
源頭找到.
解決方案
參照https://juejin.im/post/5902e087da2f60005df05c3d.
mysql> set global time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec)
mysql> set time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec)
告知運維設定時區,重啟MySql
服務,問題解決.
此外,作為防禦措施,可以在jdbc url
中設定時區(如此設定可以不用修改MySql
配置):
jdbc:mysql://localhost:3306/table_name?useTimezone=true&serverTimezone=GMT%2B8
此時,就告知連線進行時區轉換
,並且時區為UTC+8
.
PS:
如果您覺得我的文章對您有幫助,可以掃碼領取下紅包,謝謝!