MySQL驅(qū)動中關(guān)于時間的坑
MySQL驅(qū)動中關(guān)于時間的坑
背景:MySQL 8.0數(shù)據(jù)庫;
最近在做一個小框架,因為本身比較精簡,就沒有引入太多依賴,直接用了JDBC來操作數(shù)據(jù)庫,因為我的表中有一個datetime類型的字段,對應(yīng)的Java代碼中使用的是java.time.LocalDateTime,在處理這個日期字段的時候,就遇到了一個有趣的問題;
在我的數(shù)據(jù)庫表建好后,在Java中使用JDBC原生API實現(xiàn)了一個repository,包含一些數(shù)據(jù)庫的操作,因為代碼中有java.time.LocalDateTime字段,在使用java.sql.PreparedStatement的時候不確認java.sql.PreparedStatement.setObject(int, java.lang.Object)方法接收java.time.LocalDateTime類型的入?yún)⒑笫欠窨梢哉_處理,就寫了一小段測試用例來測試,核心代碼如下:
preparedStatement.setObject(1, LocalDateTime.now());
一行簡單的JDBC調(diào)用設(shè)置參數(shù),參數(shù)類型是LocalDateTime,最終發(fā)現(xiàn)沒有報錯,如果一切OK,那么事情到這里就結(jié)束了,但是.......

當我到數(shù)據(jù)庫查看數(shù)據(jù)的時候,發(fā)現(xiàn)日期竟然不對,日期存到數(shù)據(jù)庫后竟然少了14個小時,整整14個小時,當時的我真的是....

還好,機智的我馬上就醒悟過來了,這肯定是時區(qū)問題,我們現(xiàn)在是東8區(qū),比UTC時間是快8小時,存儲到數(shù)據(jù)庫后時間比我們現(xiàn)在慢了14個小時,也就是比UTC時間慢了6個小時,而這正好處于CST時區(qū),CST時區(qū)中的代表地區(qū)就是北美中部,難道我的服務(wù)器人被搬到了北美?

那當然不是了,應(yīng)該是MySQL的時區(qū)設(shè)置有問題,MySQL數(shù)據(jù)庫是美國開發(fā)的軟件,默認情況下時區(qū)是CST很符合邏輯,而在我登錄到服務(wù)器上之后,查看服務(wù)器時區(qū),服務(wù)器的時區(qū)是對的,然后又查看了MySQL的默認時區(qū),果然,我發(fā)現(xiàn)MySQL的默認時區(qū)設(shè)置是CST,使用如下代碼將其修改為UTC+8后就正常了:
set global time_zone = '+8:00';
flush privileges;
# 使用上述命令修改完畢后使用下面的命令查看修改是否成功
show variables where Variable_name like "%time_zone%";
如果事情到這里就結(jié)束了,那這也太簡單了,而且跟標題似乎沒有半毛錢關(guān)系呀?別急,其實在執(zhí)行上一步修改時區(qū)之前,機智的我忽然想起來我還有另外一個項目,也用到了java.time.LocalDateTime,并且使用的測試數(shù)據(jù)庫與我當前這個項目是同一個,但是那個項目從來沒有發(fā)現(xiàn)過這個問題,難道是因為那個項目中使用了mybatis,mybatis對此做了什么特殊處理?想到這里,筆者趕緊去翻了下mybatis相關(guān)源碼,發(fā)現(xiàn)mybatis對于LocalDateTime也是直接調(diào)用了java.sql.PreparedStatement.setObject(int, java.lang.Object)方法將LocalDateTime傳給了MySQL驅(qū)動,沒有做任何處理,那怎么就沒問題呢?
既然想不明白,那就debug一下吧,看下在我們JDBC版本的項目中debug LocalDateTime參數(shù)是如何傳輸?shù)組ySQL的,最終debug到如下關(guān)鍵源碼:
代碼在com.mysql.cj.ClientPreparedQueryBindings.setTimestamp(int, java.sql.Timestamp)處:
// PS:這里x是Timestamp類型的,在上層調(diào)用時將LocalDateTime轉(zhuǎn)換為了Timestamp類型,這個不影響我們的分析
setTimestamp(parameterIndex, x, this.session.getServerSession().getDefaultTimeZone());
在上述代碼中this.session.getServerSession().getDefaultTimeZone()最終返回了MySQL數(shù)據(jù)庫中的默認時區(qū),因為我們MySQL中默認時區(qū)是CST,所以這里也符合我們觀察到的現(xiàn)象,最終保存到數(shù)據(jù)庫的日期比實際我們當前的東8區(qū)慢14個小時,但是為什么使用mybatis的另外一個項目就沒有問題呢,要知道這兩個項目使用的測試數(shù)據(jù)庫是同一個的,這就很奇怪,到這里,我決定再在另一個項目中也debug一下,看下問題到底出在了哪里;
然后我就在使用mybatis版本的項目中做了一個插入測試,然后debug看是怎么處理的,最終debug到如下源碼處:
代碼在com.mysql.cj.ClientPreparedQueryBindings#setLocalDateTime處,關(guān)鍵代碼如下:
formatter = new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd HH:mm:ss").appendFraction(ChronoField.NANO_OF_SECOND, 0, 6, true)
.toFormatter();
sb = new StringBuilder("'");
sb.append(x.format(formatter));
sb.append("'");
setValue(parameterIndex, sb.toString(), targetMysqlType);
可以看到這里并沒有使用MySQL數(shù)據(jù)庫的時區(qū)信息,所以肯定不會出錯,但是這怎么跟沒有使用mybatis的項目行為不一致呢?

就在這時,我忽然瞥見了IDEA上方tab標簽上有兩個ClientPreparedQueryBindings文件,如下所示(這里是給其他不必要的文件關(guān)了后的效果,為了更好看出效果):

然后我瞬間明白了,這個問題與mybatis沒有任何關(guān)系,是兩個項目的MySQL驅(qū)動不一致導(dǎo)致的,使用mybatis的項目使用的MySQL驅(qū)動版本是8.0.26,其中日期處理沒有去使用MySQL服務(wù)端的時區(qū),所以使用mybatis的項目存儲到MySQL中的日期是沒問題的,而我當前這個直接使用JDBC項目的MySQL驅(qū)動版本是8.0.11,在這個版本的驅(qū)動中對于LocalDateTime的處理使用了MySQL服務(wù)端的時區(qū),這就導(dǎo)致了存儲到MySQL中日期變了;
最后,這個問題到這里有兩個最簡單的方案解決,一個是修改數(shù)據(jù)庫默認時區(qū),還有一個就是升級使用MySQL8.0.26版本的驅(qū)動,而我使用了第三種更復(fù)雜一點兒的方案:
1、將數(shù)據(jù)庫默認時區(qū)修改為東8區(qū);
2、將項目的MySQL驅(qū)動升級為8.0.26版本;
3、對LocalDateTime提前處理,在我自己的代碼中將其格式化為字符串,然后調(diào)用`java.sql.PreparedStatement.setObject(int, java.lang.Object)`將其設(shè)置進去;
為什么使用這種方案呢?
1、將數(shù)據(jù)庫默認時區(qū)修改為東8區(qū)防止其他人再出相同問題,也防止在其他地方有用到這個時區(qū)數(shù)據(jù)時出錯;
2、將項目的MySQL驅(qū)動升級為8.0.26是因為其他項目已經(jīng)在用這個版本的驅(qū)動了,盡量保持一致防止再出一些其他兼容性問題;
3、對LocalDateTime提前處理,在自己的代碼中將其格式化為字符串而不是等待MySQL驅(qū)動去格式化它,防止未來某天升級MySQL驅(qū)動時MySQL驅(qū)動行為再次修改導(dǎo)致出現(xiàn)問題;
最后,建議大家沒有必要不要隨意升級jar包,特別是這種涉及底層驅(qū)動的jar包,防止出現(xiàn)兼容性問題;
聯(lián)系我
作者微信:JoeKerouac
微信公眾號(文章會第一時間更新到公眾號,如果搜不出來可能是改名字了,加微信即可=_=|):代碼深度研究院
GitHub:https://github.com/JoeKerouac
