摘要
線上排查問題時(shí)候碰到一個(gè)奇怪的問題怖竭,代碼中讀取一天的記錄。代碼中設(shè)置時(shí)間是從零點(diǎn)到夜里二十四點(diǎn)陡蝇。但是讀取出來的記錄的開始是既然是從13點(diǎn)開始的痊臭。然后看了JDBC的源碼發(fā)現(xiàn)主要原因是Mysql的CST時(shí)間與Java中CST時(shí)間是不一樣的,下面給出問題的排查過程登夫。
情景再現(xiàn)
1广匙、代碼中用的java.util.Date類型、換成TimeStamp類型也沒有解決問題
2恼策、數(shù)據(jù)庫中用的TimeStamp類型
3鸦致、mysql 版本5.6.x
4、jdk版本1.8
5涣楷、mysql-connector-java 8.0.13
我的數(shù)據(jù)庫時(shí)區(qū)信息如下:
select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| CST |
+--------------------+
1 row in set (4.81 sec)
mysql> select @@time_zone;
+-------------+
| @@time_zone |
+-------------+
| SYSTEM |
+-------------+
1 row in set (0.04 sec)
CST時(shí)間
CST時(shí)間有四種解釋分唾,所以不同項(xiàng)目中可能代碼的意義不一樣,比如Mysql和Java狮斗。這也是這次錯(cuò)誤的主要原因绽乔。Java和Mysql協(xié)商時(shí)區(qū)時(shí)把Mysql的CST時(shí)間當(dāng)成了美國中部時(shí)間既UTC-5(美國從“3月11日”至“11月7日”實(shí)行夏令時(shí),美國中部時(shí)間改為 UTC-05:00,其他時(shí)候是UTC-06:00)情龄。我們國家是UTC+08:00 時(shí)區(qū)迄汛,所以差了13個(gè)小時(shí)(13小時(shí)還是14小時(shí)捍壤,取決于你傳遞給數(shù)據(jù)庫的時(shí)間)骤视,
- 美國中部時(shí)間 Central Standard Time (USA) UTC-05:00 / UTC-06:00
- 澳大利亞中部時(shí)間 Central Standard Time (Australia) UTC+09:30
- 中國標(biāo)準(zhǔn)時(shí) China Standard Time UTC+08:00
- 古巴標(biāo)準(zhǔn)時(shí) Cuba Standard Time UTC-04:00
CST in Java
SimpleDateFormat f1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
f1.setTimeZone(TimeZone.getTimeZone("CST"));
System.out.println(f1.parse("2015-09-01 00:00:00"));
上面代碼的輸出如下:
Tue Sep 01 13:00:00 CST 2015
比實(shí)際的時(shí)間多了13個(gè)小時(shí)。所以你在Java中的時(shí)間被認(rèn)為是UTC-5時(shí)間鹃觉,而數(shù)據(jù)庫任務(wù)時(shí)間是UTC-8時(shí)間专酗。這就是我們上面錯(cuò)誤的原因。
源碼分析
關(guān)鍵的代碼在ConnectionImpl類中盗扇,代碼如下祷肯,已經(jīng)去掉一些無關(guān)重要的代碼
private void initializePropsFromServer() throws SQLException {
String connectionInterceptorClasses = this.propertySet.getStringProperty(PropertyKey.connectionLifecycleInterceptors).getStringValue();
this.session.setSessionVariables();
this.session.loadServerVariables(this.getConnectionMutex(), this.dbmd.getDriverVersion()); //查詢數(shù)據(jù)庫一些重要的系統(tǒng)配置
this.autoIncrementIncrement = this.session.getServerSession().getServerVariable("auto_increment_increment", 1);
this.session.buildCollationMapping();
this.session.getProtocol().initServerSession();// 初始化會(huì)話,協(xié)商時(shí)區(qū)代碼就在里面
checkTransactionIsolationLevel();
this.session.checkForCharsetMismatch();
this.session.configureClientCharacterSet(false);
handleAutoCommitDefaults();
其中this.session.loadServerVariables(this.getConnectionMutex(), this.dbmd.getDriverVersion());查詢Mysql重要配置疗隶。比如時(shí)區(qū)佑笋。具體查詢的信息如下:
StringBuilder queryBuf = new StringBuilder(versionComment).append("SELECT");
queryBuf.append(" @@session.auto_increment_increment AS auto_increment_increment");
queryBuf.append(", @@character_set_client AS character_set_client");
queryBuf.append(", @@character_set_connection AS character_set_connection");
queryBuf.append(", @@character_set_results AS character_set_results");
queryBuf.append(", @@character_set_server AS character_set_server");
queryBuf.append(", @@collation_server AS collation_server");
queryBuf.append(", @@collation_connection AS collation_connection");
queryBuf.append(", @@init_connect AS init_connect");
queryBuf.append(", @@interactive_timeout AS interactive_timeout");
if (!versionMeetsMinimum(5, 5, 0)) {
queryBuf.append(", @@language AS language");
}
queryBuf.append(", @@license AS license");
queryBuf.append(", @@lower_case_table_names AS lower_case_table_names");
queryBuf.append(", @@max_allowed_packet AS max_allowed_packet");
queryBuf.append(", @@net_write_timeout AS net_write_timeout");
if (!versionMeetsMinimum(8, 0, 3)) {
queryBuf.append(", @@query_cache_size AS query_cache_size");
queryBuf.append(", @@query_cache_type AS query_cache_type");
}
queryBuf.append(", @@sql_mode AS sql_mode");
queryBuf.append(", @@system_time_zone AS system_time_zone");
queryBuf.append(", @@time_zone AS time_zone");
if (versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))) {
queryBuf.append(", @@transaction_isolation AS transaction_isolation");
} else {
queryBuf.append(", @@tx_isolation AS transaction_isolation");
}
queryBuf.append(", @@wait_timeout AS wait_timeout");
this.session.getProtocol().initServerSession();這就是協(xié)商時(shí)區(qū)的代碼,也是我們重點(diǎn)需要關(guān)注的代碼斑鼻。如下
public void configureTimezone() {
// 獲取mysql時(shí)區(qū)配置蒋纬,結(jié)果是SYSTEM
String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone");
//因?yàn)槲业臄?shù)據(jù)庫time_zone是SYSTEM,所以就使用system_time_zone作為數(shù)據(jù)的時(shí)區(qū),如一開始mysql查詢結(jié)果蜀备,時(shí)區(qū)為CST关摇,既configuredTimeZoneOnServer=CST
if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) {
configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone");
}
// 從配置中查找你對時(shí)區(qū)的配置,如果你沒有這里為null碾阁。getPropertySet()就保存了你的數(shù)據(jù)庫用戶名输虱、密碼、字符編碼啊等你在url鏈接中設(shè)置的屬性
String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue();
//因?yàn)槲覜]有配置serverTimezone屬性脂凶,所以canonicalTimezone==null
if (configuredTimeZoneOnServer != null) {
// user can override this with driver properties, so don't detect if that's the case
if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) {
try {
//協(xié)商java中的時(shí)區(qū)宪睹,因?yàn)镸ysql為CST,所以這里也是CST
canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor());
} catch (IllegalArgumentException iae) {
throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor());
}
}
}
if (canonicalTimezone != null && canonicalTimezone.length() > 0) {
//將剛剛得到的Java的時(shí)區(qū)設(shè)置到會(huì)話中
this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone));
}
this.serverSession.setDefaultTimeZone(this.serverSession.getServerTimeZone());
}
再來看一下蚕钦,如果給sql語句的占位符中傳遞值的時(shí)候代碼
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);
代碼中把Date或者TimeStamp轉(zhuǎn)換為String横堡,而且用了協(xié)商的時(shí)區(qū)。
分析到這里冠桃,問題基本上說清楚了命贴。那么我們?nèi)绾谓鉀Q這個(gè)問題呢?
總結(jié)
1食听、數(shù)據(jù)庫時(shí)區(qū)最好不要設(shè)置成CST胸蛛,以免出現(xiàn)上面的錯(cuò)誤
2、當(dāng)數(shù)據(jù)庫中的時(shí)間用的是時(shí)間類型時(shí)候樱报,Java中可以用String葬项,但是不適應(yīng)做國際化
3、在數(shù)據(jù)庫連接字符串中設(shè)置時(shí)區(qū)迹蛤。如下(推薦的方式):
jdbc:mysql://xxxx:3306/table_name?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8