成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

數據庫時間慢了14個小時,Mybatis說,這個鍋我不背!

運維 數據庫運維
大家都知道,對于這類Bug本人是很感興趣的。直覺告訴我,應該不是Mybatis的Bug,很可能是時區的問題。很好,今天又可以帶大家一起來排查Bug了,看看從這次的Bug排查中你能Get什么技能。

[[436824]]

本文轉載自微信公眾號「程序新視界」,作者二師兄。轉載本文請聯系程序新視界公眾號。

同事反饋一個問題:Mybatis插入數據庫的時間是昨天的,是不是因為生成Mybatis逆向工程生成的代碼有問題?

大家都知道,對于這類Bug本人是很感興趣的。直覺告訴我,應該不是Mybatis的Bug,很可能是時區的問題。

很好,今天又可以帶大家一起來排查Bug了,看看從這次的Bug排查中你能Get什么技能。

這次研究的問題有點深奧,但結論很重要。Let's go!

問題猜想

同事反饋問題的時候,帶了自己的猜想:是不是數據庫字段設置為datetime導致?是不是Mybatis逆向工程生成的代碼中類型不一致導致的?

同事還要把datetime改為varchar……馬上被我制止了,說:先排查問題,再說解決方案,下午我也抽時間看看。

問題核查

第一步,檢查數據庫字段類型,是datetime的,沒問題。

第二步,檢查實體類中類型,是java.util.Date類型,沒問題。

第三步,Bug復現。

在Bug復現這一步,用到了單元測試。話說之前還跟朋友討論過單元測試的魅力,現在本人是越來越喜歡單元測試了。

項目基于Spring Boot的,單元測試如下(代碼已脫敏):

  1. @SpringBootTest 
  2. class DateTimeTests { 
  3.  
  4.  @Resource 
  5.  private UserMapper userMapper; 
  6.  
  7.  @Test 
  8.  public void testDate(){ 
  9.   User  user = new User(); 
  10.   // 省略其他字段 
  11.   user.setCreateDate(new Date()); 
  12.   userMapper.insertSelective(user); 
  13.  } 

執行單元測試,查看數據庫中插入的數據。Bug復現,時間的確是前一天的,與當前時間相差14個小時。

經過上面三步的排查,核實了數據庫字段和代碼中類型沒問題。單元測試也復現了問題,同事沒有欺騙我,總要眼見為實,哈哈。

于是基本確定是時區問題。

時區排查

檢查服務器時間

登錄測試服務器,執行date命令,檢查服務器時間和時區:

  1. [root@xxx ~]# date 
  2. 2021年 11月 25日 星期四 09:26:25 CST 
  3. [root@xxx ~]# date -R 
  4. Thu, 25 Nov 2021 09:33:34 +0800 

顯示時間是當前時間,采用CST時間,最后的+0800,即東8區,沒問題。

檢查數據庫時區

連接數據庫,執行show命令:

  1. show variables like '%time_zone%'
  2.  
  3. +----------------------------+ 
  4. |Variable         | Value | 
  5. +----------------------------+ 
  6. |system_time_zone |CST    | 
  7. |time_zone       |SYSTEM | 
  • system_time_zone:全局參數,系統時區,在MySQL啟動時會檢查當前系統的時區并根據系統時區設置全局參數system_time_zone的值。值為CST,與系統時間的時區一致。
  • time_zone:全局參數,設置每個連接會話的時區,默認為SYSTEM,使用全局參數system_time_zone的值。

檢查代碼中時區

在單元測試的方法內再添加打印時區的代碼:

  1. @Test 
  2.  public void testDate(){ 
  3.   System.out.println(System.getProperty("user.timezone")); 
  4.   User  user = new User(); 
  5.   // 省略其他字段 
  6.   user.setCreateDate(new Date()); 
  7.   userMapper.insertSelective(user); 
  8.  } 

打印的時區為:

  1. Asia/Shanghai 

也就是說Java中使用的是UTC時區進行業務邏輯處理的,也是東八區的時間。

那么問題到底出在哪里呢?

問題基本呈現

經過上述排查,基本上確定是時區的問題。這里,再補充一下上述相關的時區知識點。

UTC時間

UTC時間:世界協調時間(UTC)是世界上不同國家用來調節時鐘和時間的主要時間標準,也就是零時區的時間。

UTC, Coordinated Universal Time是一個標準,而不是一個時區。UTC 是一個全球通用的時間標準。全球各地都同意將各自的時間進行同步協調 (coordinated),這也是UTC名字的來源:Universal Coordinated Time。

CST時間

CST時間:中央標準時間。

CST可以代表如下4個不同的時區:

  • Central Standard Time (USA) UT-6:00,美國
  • Central Standard Time (Australia) UT+9:30,澳大利亞
  • China Standard Time UT+8:00,中國
  • Cuba Standard Time UT-4:00,古巴

再次分析

很顯然,這里與UTC時間無關,它只是時間標準。目前Mysql中的system_time_zone是CST,而CST可以代表4個不同的時區,那么,Mysql把它當做哪個時區進行處理了呢?

簡單推算一下,中國時間是UT+8:00,美國是 UT-6:00,當傳入中國時間,直接轉換為美國時間(未考慮時區問題),時間便慢了14個小時。

既然知道了問題,那么解決方案也就有了。

解決方案

針對上述問題可通過數據庫層面和代碼層面進行解決。

方案一:修改數據庫時區

既然是MySQL理解錯了CST指定的時區,那么就將其設置為正確的。

連接Mysql數據庫,設置正確的時區:

  1. [root@xxxxx ~]# mysql -uroot -p 
  2.  
  3. mysql> set global time_zone = '+8:00'
  4.  
  5. mysql> set time_zone = '+8:00' 
  6.  
  7. mysql> flush privileges

再次執行show命令:

  1. show variables like '%time_zone%'
  2.  
  3. +----------------------------+ 
  4. |Variable         | Value | 
  5. +----------------------------+ 
  6. |system_time_zone |CST    | 
  7. |time_zone       |+08:00 | 

可以看到時區已經成為東八區的時間了。再次執行單元測試,問題得到解決。

此種方案也可以直接修改MySQL的my.cnf文件進行指定時區。

方案二:修改數據庫連接參數

在代碼連接數據庫時,通過參數指定所使用的時區。

在配置數據庫連接的URL后面添加上指定的時區serverTimezone=Asia/Shanghai:

  1. url: jdbc:mysql://xx.xx.xx.xx:3306/db_name?useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai 

再次執行單元測試,問題同樣可以得到解決。

問題完了?

經過上述分析與操作,時區的問題已經解決了。問題就這么完事了嗎?為什么是這樣呢?

為了驗證時區問題,在時區錯誤的數據庫中,創建了一個字段,該字段類型為datetime,默認值為CURRENT_TIMESTAMP。

那么,此時插入一條記錄,讓Mysql自動生成該字段的時間,你猜該字段的時間是什么?中國時間。

神奇不?為什么同樣是CST時區,系統自動生成的時間是正確的,而代碼插入的時間就有時差問題呢?

到底是Mysql將CST時區理解為美國時間了,還是Mybatis、連接池或驅動程序將其理解為美國時間了?

重頭戲開始

為了追查到底是代碼中哪里出了問題,先開啟Mybatis的debug日志,看看insert時是什么值:

  1. 2021-11-25 11:05:28.367 [|1637809527983|] DEBUG 20178 --- [   scheduling-1] c.h.s.m.H.listByCondition                : ==> Parameters: 2021-11-25 11:05:27(String), 0(Integer), 1(Integer), 2(Integer), 3(Integer), 4(Integer) 

上面是insert時的參數,也就是說在Mybatis層面時間是沒問題的。排除一個。

那是不是連接池或驅動程序的問題?連接池本身來講跟數據庫連接的具體操作關系不大,就直接來排查驅動程序。

Mybatis是xml中定義日期字段類型為TIMESTAMP,扒了一下mysql-connector-Java-8.0.x的源碼,發現SqlTimestampValueFactory是用來處理TIMESTAMP類型的。

在SqlTimestampValueFactory的構造方法上打上斷點,執行單元測試:

timezone

可以明確的看到,Calendar將時區設置為Locale.US,也就是美國時間,時區為CST,offset為-21600000。-21600000單位為毫秒,轉化為小時,恰好是“-6:00”,這與北京時間“GMT+08:00”恰好相差14個小時。

于是一路往上最終追溯調用鏈路,該TimeZone來自NativeServerSession的serverTimeZone,而serverTimeZone的值是由NativeProtocol類的configureTimezone方法設置的。

  1. public void configureTimezone() { 
  2.         String configuredTimeZoneOnServer = this.serverSession.getServerVariable("time_zone"); 
  3.  
  4.         if ("SYSTEM".equalsIgnoreCase(configuredTimeZoneOnServer)) { 
  5.             configuredTimeZoneOnServer = this.serverSession.getServerVariable("system_time_zone"); 
  6.         } 
  7.  
  8.         String canonicalTimezone = getPropertySet().getStringProperty(PropertyKey.serverTimezone).getValue(); 
  9.  
  10.         if (configuredTimeZoneOnServer != null) { 
  11.             // user can override this with driver properties, so don't detect if that's the case 
  12.             if (canonicalTimezone == null || StringUtils.isEmptyOrWhitespaceOnly(canonicalTimezone)) { 
  13.                 try { 
  14.                     canonicalTimezone = TimeUtil.getCanonicalTimezone(configuredTimeZoneOnServer, getExceptionInterceptor()); 
  15.                 } catch (IllegalArgumentException iae) { 
  16.                     throw ExceptionFactory.createException(WrongArgumentException.class, iae.getMessage(), getExceptionInterceptor()); 
  17.                 } 
  18.             } 
  19.         } 
  20.  
  21.         if (canonicalTimezone != null && canonicalTimezone.length() > 0) { 
  22.          // 此處設置TimeZone 
  23.             this.serverSession.setServerTimeZone(TimeZone.getTimeZone(canonicalTimezone)); 
  24.  
  25.             if (!canonicalTimezone.equalsIgnoreCase("GMT") && this.serverSession.getServerTimeZone().getID().equals("GMT")) { 
  26.                 throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("Connection.9", new Object[] { canonicalTimezone }), 
  27.                         getExceptionInterceptor()); 
  28.             } 
  29.         } 
  30.  
  31.     } 

debug跟蹤一下上述代碼,顯示信息如下:

CST獲得

至此,通過canonicalTimezone值的獲取,可以看出URL后面配置serverTimezone=Asia/Shanghai的作用了。其中,上面第一個代碼塊獲取time_zone的值,第二個代碼塊中獲取system_time_zone的值。這與查詢數據庫獲得的值一致。

因為出問題時并未在url中添加參數serverTimezone=Asia/Shanghai,所以走canonicalTimezone為null的情況。隨后邏輯中調用了TimeUtil.getCanonicalTimezone方法:

  1. public static String getCanonicalTimezone(String timezoneStr, ExceptionInterceptor exceptionInterceptor) { 
  2.         if (timezoneStr == null) { 
  3.             return null
  4.         } 
  5.  
  6.         timezoneStr = timezoneStr.trim(); 
  7.  
  8.         // handle '+/-hh:mm' form ... 
  9.         if (timezoneStr.length() > 2) { 
  10.             if ((timezoneStr.charAt(0) == '+' || timezoneStr.charAt(0) == '-') && Character.isDigit(timezoneStr.charAt(1))) { 
  11.                 return "GMT" + timezoneStr; 
  12.             } 
  13.         } 
  14.  
  15.         synchronized (TimeUtil.class) { 
  16.             if (timeZoneMappings == null) { 
  17.                 loadTimeZoneMappings(exceptionInterceptor); 
  18.             } 
  19.         } 
  20.  
  21.         String canonicalTz; 
  22.         if ((canonicalTz = timeZoneMappings.getProperty(timezoneStr)) != null) { 
  23.             return canonicalTz; 
  24.         } 
  25.  
  26.         throw ExceptionFactory.createException(InvalidConnectionAttributeException.class, 
  27.                 Messages.getString("TimeUtil.UnrecognizedTimezoneId", new Object[] { timezoneStr }), exceptionInterceptor); 
  28.     } 

上述代碼中最終走到了loadTimeZoneMappings(exceptionInterceptor);方法:

  1. private static void loadTimeZoneMappings(ExceptionInterceptor exceptionInterceptor) { 
  2.         timeZoneMappings = new Properties(); 
  3.         try { 
  4.             timeZoneMappings.load(TimeUtil.class.getResourceAsStream(TIME_ZONE_MAPPINGS_RESOURCE)); 
  5.         } catch (IOException e) { 
  6.             throw ExceptionFactory.createException(Messages.getString("TimeUtil.LoadTimeZoneMappingError"), exceptionInterceptor); 
  7.         } 
  8.         // bridge all Time Zone ids known by Java 
  9.         for (String tz : TimeZone.getAvailableIDs()) { 
  10.             if (!timeZoneMappings.containsKey(tz)) { 
  11.                 timeZoneMappings.put(tz, tz); 
  12.             } 
  13.         } 
  14.     } 

該方法加載了配置文件"/com/mysql/cj/util/TimeZoneMapping.properties"里面的值,經過轉換,timeZoneMappings中,對應CST的為"CST"。

最終得到canonicalTimezone為“CST”,而TimeZone獲得是通過TimeZone.getTimeZone(canonicalTimezone)方法獲得的。

也就是說TimeZone.getTimeZone("CST")的值為美國時間。寫個單元測試驗證一下:

  1. public class TimeZoneTest { 
  2.  
  3.  @Test 
  4.  public void testTimeZone(){ 
  5.   System.out.println(TimeZone.getTimeZone("CST")); 
  6.  } 

打印結果:

  1. sun.util.calendar.ZoneInfo[id="CST",offset=-21600000,dstSavings=3600000,useDaylight=true,transitions=235,lastRule=java.util.SimpleTimeZone[id=CST,offset=-21600000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]] 

很顯然,該方法傳入CST之后,默認是美國時間。

至此,問題原因基本明朗:

  • Mysql中設置的server_time_zone為CST,time_zone為SYSTEM。
  • Mysql驅動查詢到time_zone為SYSTEM,于是使用server_time_zone的值,為”CST“。
  • JDK中TimeZone.getTimeZone("CST")獲得的值為美國時區;
  • 以美國時區構造的Calendar類;
  • SqlTimestampValueFactory使用上述Calendar來格式化系統獲取的中國時間,時差問題便出現了;
  • 最終反映在數據庫數據上就是錯誤的時間。

serverVariables變量

再延伸一下,其中server_time_zone和time_zone都來自于NativeServerSession的serverVariables變量,該變量在NativeSession的loadServerVariables方法中進行初始化,關鍵代碼:

  1. if (versionMeetsMinimum(5, 1, 0)) { 
  2.                 StringBuilder queryBuf = new StringBuilder(versionComment).append("SELECT"); 
  3.                 queryBuf.append("  @@session.auto_increment_increment AS auto_increment_increment"); 
  4.                 queryBuf.append(", @@character_set_client AS character_set_client"); 
  5.                 queryBuf.append(", @@character_set_connection AS character_set_connection"); 
  6.                 queryBuf.append(", @@character_set_results AS character_set_results"); 
  7.                 queryBuf.append(", @@character_set_server AS character_set_server"); 
  8.                 queryBuf.append(", @@collation_server AS collation_server"); 
  9.                 queryBuf.append(", @@collation_connection AS collation_connection"); 
  10.                 queryBuf.append(", @@init_connect AS init_connect"); 
  11.                 queryBuf.append(", @@interactive_timeout AS interactive_timeout"); 
  12.                 if (!versionMeetsMinimum(5, 5, 0)) { 
  13.                     queryBuf.append(", @@language AS language"); 
  14.                 } 
  15.                 queryBuf.append(", @@license AS license"); 
  16.                 queryBuf.append(", @@lower_case_table_names AS lower_case_table_names"); 
  17.                 queryBuf.append(", @@max_allowed_packet AS max_allowed_packet"); 
  18.                 queryBuf.append(", @@net_write_timeout AS net_write_timeout"); 
  19.                 queryBuf.append(", @@performance_schema AS performance_schema"); 
  20.                 if (!versionMeetsMinimum(8, 0, 3)) { 
  21.                     queryBuf.append(", @@query_cache_size AS query_cache_size"); 
  22.                     queryBuf.append(", @@query_cache_type AS query_cache_type"); 
  23.                 } 
  24.                 queryBuf.append(", @@sql_mode AS sql_mode"); 
  25.                 queryBuf.append(", @@system_time_zone AS system_time_zone"); 
  26.                 queryBuf.append(", @@time_zone AS time_zone"); 
  27.                 if (versionMeetsMinimum(8, 0, 3) || (versionMeetsMinimum(5, 7, 20) && !versionMeetsMinimum(8, 0, 0))) { 
  28.                     queryBuf.append(", @@transaction_isolation AS transaction_isolation"); 
  29.                 } else { 
  30.                     queryBuf.append(", @@tx_isolation AS transaction_isolation"); 
  31.                 } 
  32.                 queryBuf.append(", @@wait_timeout AS wait_timeout"); 
  33.  
  34.                 NativePacketPayload resultPacket = sendCommand(this.commandBuilder.buildComQuery(null, queryBuf.toString()), false, 0); 
  35.                 Resultset rs = ((NativeProtocol) this.protocol).readAllResults(-1, false, resultPacket, falsenull
  36.                         new ResultsetFactory(Type.FORWARD_ONLY, null)); 
  37.                 Field[] f = rs.getColumnDefinition().getFields(); 
  38.                 if (f.length > 0) { 
  39.                     ValueFactory<String> vf = new StringValueFactory(this.propertySet); 
  40.                     Row r; 
  41.                     if ((r = rs.getRows().next()) != null) { 
  42.                         for (int i = 0; i < f.length; i++) { 
  43.                             this.protocol.getServerSession().getServerVariables().put(f[i].getColumnLabel(), r.getValue(i, vf)); 
  44.                         } 
  45.                     } 
  46.                 } 

在上述StringBuilder的append操作中,有"@@time_zone AS time_zone"和"@@system_time_zone AS system_time_zone"兩個值,然后查詢數據庫,從數據庫獲得值之后,put到serverVariables中。

再來debug一下:

system_time_zone

可以看出system_time_zone的值為CST。

time_zone

同樣time_zone的值為“SYSTEM”。

根據代碼中的提示,拼接與代碼一樣的SQL查詢一下數據庫:

  1. select @@time_zone; 
  2.  
  3. SYSTEM 

值的確是“SYSTEM”。此時,我們又得出另外一個查詢Mysql當前時區的方法。

至此,該問題的排查完美收官。大出一口氣。

小結

在上述問題排查的過程中,多次用到單元測試,這也是單元測試的魅力所在,用最簡單的代碼,最輕量的邏輯,最節省時間的方式來驗證和追蹤錯誤。

再回顧一下上述Bug排查中用到和學到的知識點:

  • Linux日期查看,時區查看及衍生如何配置時區;
  • Mysql時區查看;
  • Spring Boot單元測試;
  • Java時區獲取;
  • UTC時間和CST時間;
  • 兩種解決時區問題的方案;
  • 閱讀、debug Mysql驅動源代碼;
  • TimeZone.getTimeZone("CST")默認時區為美國時區;
  • Mysql驅動中處理時區問題基本流程邏輯;
  • Mybatis debug日志相關打印;
  • 其他相關知識。 

通過本篇Bug查找的文章,你學到了什么?如果有那么一點啟發,不要吝嗇,給點個贊吧!

 

責任編輯:武曉燕 來源: 程序新視界
相關推薦

2017-09-12 16:18:22

ICO區塊鏈互聯網+

2020-02-20 16:21:46

遠程辦公

2019-06-03 14:38:11

AWS挖掘機光纜

2015-12-09 13:51:21

Intelskylake散熱器

2024-08-05 08:00:53

2021-04-13 17:38:38

區塊鏈比特幣安全

2018-10-19 16:35:20

運維

2019-12-03 13:57:38

CIO背鍋IT

2018-05-08 09:49:15

數據庫運維優化

2018-05-02 14:30:33

數據庫運維優化故障

2019-01-16 18:11:28

程序員技能開發者

2021-04-16 09:20:34

黑客DDoS網絡攻擊

2017-08-23 17:11:40

WI-FI流量路由器

2019-12-17 10:01:40

開發技能代碼

2023-04-26 07:16:25

游戲掉幀CPU

2017-06-09 13:36:33

人工智能深度學習

2017-09-25 10:52:27

2020-12-09 11:00:44

Nginx 運維Tomcat

2021-11-03 16:25:26

Linux磁盤空間命令

2025-03-24 08:00:00

數據庫開發代碼
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 成人午夜黄色 | 欧美日韩综合视频 | 亚洲欧美日韩精品久久亚洲区 | www.youjizz.com日韩 | 91精品国产综合久久久动漫日韩 | 亚洲激情专区 | 视频三区| 日韩电影免费在线观看中文字幕 | 五月婷婷 六月丁香 | 一本综合久久 | 九九热精 | 久久精品国产免费高清 | 日日天天 | 婷婷桃色网 | 日韩精品一区二区三区免费观看 | 国产成人99久久亚洲综合精品 | 精品国产乱码久久久久久果冻传媒 | 三级黄色片在线观看 | 国产高清久久久 | 日韩中文字幕在线观看 | 黄a网站| 蜜臀av日日欢夜夜爽一区 | 91久久国产综合久久 | 精品欧美 | 成人av在线播放 | 国产91亚洲精品一区二区三区 | 国产精品99久久久久久久vr | 污片在线免费观看 | 欧美综合网| 亚洲乱码国产乱码精品精的特点 | 日韩在线观看一区二区三区 | 亚洲综合天堂 | 日韩成人av在线 | 久久69精品久久久久久久电影好 | 亚洲欧美中文日韩在线v日本 | 精品福利一区二区三区 | 中文字幕在线中文 | 精品久久久久久亚洲综合网站 | 国产一区二区三区久久久久久久久 | 久久精品免费看 | 中文字幕国产精品 |