MySQL 盡量避免使用 TIMESTAMP!!

MySQL 中常見(jiàn)的時(shí)間類型有三種DATE, DATETIME和 TIMESTAMP,其中DATE類型用于表示日期,但是不會(huì)包含時(shí)間,格式為YYYY-MM-DD,而DATETIME和TIMESTAMP用于表示日期和時(shí)間,常見(jiàn)的格式為YYYY-MM-DD HH:MM:SS,也可以帶6位小數(shù)來(lái)表示微秒。
不同于DATETIME,TIMESTAMP支持的時(shí)間范圍從1970-01-01 00:00:01.000000到2038-01-19 03:14:07.999999,使用了TIMESTAMP的應(yīng)用很有可能在2038-01-19 03:14:07.999999之后宕機(jī),同樣面臨這個(gè)問(wèn)題的還有所有的類Unix系統(tǒng),因?yàn)樗麄兪褂昧?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">time_t這一32位數(shù)字來(lái)表示時(shí)間,這就是著名的2038年問(wèn)題。
因?yàn)闀r(shí)間問(wèn)題搞壞系統(tǒng)的例子可不少,在2016年曾經(jīng)爆出過(guò)一個(gè)iPhone的bug,如果將iPhone的時(shí)間調(diào)整到1970-01-01 00:00:00,則會(huì)導(dǎo)致手機(jī)”變磚“,原因是IOS基于BSD這種Unix系統(tǒng)構(gòu)建,在將時(shí)間調(diào)整到1970-01-01 00:00:00后,如果手機(jī)需要展示之前的時(shí)間,例如之前收到過(guò)短信,則會(huì)導(dǎo)致整數(shù)溢出。
對(duì)于2038問(wèn)題,Linux的解法是提供新的用戶接口:https://kernelnewbies.org/y2038.但是MySql至今還沒(méi)有相應(yīng)的公告。
TIMESTAMP的設(shè)計(jì)之初是為了支持自動(dòng)時(shí)區(qū)轉(zhuǎn)換:
mysql>?CREATE?TABLE?`employee`?(
????->??`entry_time`?timestamp?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP
????->?)?ENGINE=InnoDB
????->?;
Query?OK,?0?rows?affected?(0.01?sec)
mysql>?INSERT?INTO?`employee`?(`entry_time`)?VALUES?(CURRENT_TIMESTAMP);
Query?OK,?1?row?affected?(0.01?sec)
mysql>?SELECT?*?FROM?`employee`;
+---------------------+
|?entry_time??????????|
+---------------------+
|?2021-05-09?08:14:08?|
+---------------------+
1?row?in?set?(0.00?sec)
mysql>?SET?@@session.time_zone?=?'-05:00';?SELECT?*?FROM?`employee`;
Query?OK,?0?rows?affected?(0.00?sec)
+---------------------+
|?entry_time??????????|
+---------------------+
|?2021-05-09?03:14:08?|
+---------------------+
1?row?in?set?(0.00?sec)
但是TIMESTAMP的一些設(shè)計(jì)卻非常鬼畜,比如:
如果表中包含 TIMESTAMP的列,那么其建表語(yǔ)句有可能被系統(tǒng)篡改,取決于MySql的版本和參數(shù)設(shè)置。當(dāng) MySQL參數(shù)time_zone=system時(shí),高并發(fā)可能會(huì)引起CPU使用率暴漲,系統(tǒng)響應(yīng)變慢甚至假死如果存入超過(guò)范圍的時(shí)間,在非嚴(yán)格狀態(tài)下,MySql不會(huì)報(bào)錯(cuò),反而會(huì)插入 '0000-00-00 00:00:00'
新建一個(gè)包含TIMESTAMP的表可真難
MySql 5.6.6版本引入了explicit_defaults_for_timestamp這個(gè)參數(shù),隨即被標(biāo)記為廢棄,這個(gè)參數(shù)主要影響表中類型為TIMESTAMP的那些列在新建表時(shí)的表現(xiàn)
mysql>?show?variables?like?'explicit_defaults_for_timestamp';
+---------------------------------+-------+
|?Variable_name???????????????????|?Value?|
+---------------------------------+-------+
|?explicit_defaults_for_timestamp?|?OFF???|
+---------------------------------+-------+
mysql>?create?table?t1?
????->?(
????->?ts1?timestamp,
????->?ts2?timestamp,
????->?ts3?timestamp?default?'2010-01-01?00:00:00'
????->?);
Query?OK,?0?rows?affected?(0.03?sec)
mysql>?show?create?table?t1\G
***************************?1.?row?***************************
???????Table:?t1
Create?Table:?CREATE?TABLE?`t1`?(
??`ts1`?timestamp?NOT?NULL?DEFAULT?CURRENT_TIMESTAMP?ON?UPDATE?CURRENT_TIMESTAMP,
??`ts2`?timestamp?NOT?NULL?DEFAULT?'0000-00-00?00:00:00',
??`ts3`?timestamp?NOT?NULL?DEFAULT?'2010-01-01?00:00:00'
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8
1?row?in?set?(0.00?sec)
雖然我們輸入的建表語(yǔ)句很簡(jiǎn)單,但是MySql卻對(duì)于我們輸入的建表語(yǔ)句做了諸多的篡改:
對(duì)于表中的第一個(gè) TIMESTAMP列,系統(tǒng)自動(dòng)加了NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,這些操作對(duì)于新建表的開(kāi)發(fā)者完全是不感知的。對(duì)于表中的第二個(gè) TIMESTAMP列,系統(tǒng)自動(dòng)加了一個(gè)默認(rèn)值0000-00-00 00:00:00,這個(gè)操作同樣對(duì)于新建表的開(kāi)發(fā)者完全不感知。
在系統(tǒng)對(duì)我們的建表語(yǔ)句做了自動(dòng)修改之后,對(duì)表的插入操作可能就不會(huì)如開(kāi)發(fā)者預(yù)期的那樣:
mysql>?insert?into?t1?values?(null,null,null);
Query?OK,?1?row?affected?(0.00?sec)
mysql>?select?*?from?t1;
+---------------------+---------------------+---------------------+
|?ts1?????????????????|?ts2?????????????????|?ts3?????????????????|
+---------------------+---------------------+---------------------+
|?2021-05-09?07:47:50?|?2021-05-09?07:47:50?|?2021-05-09?07:47:50?|
+---------------------+---------------------+---------------------+
1?row?in?set?(0.00?sec)
可以看到,MySql的表現(xiàn)非常的鬼畜
對(duì)于第一個(gè) TIMESTAMP列,建表語(yǔ)句中指定可以為null,但是插入null的時(shí)候存到表里的卻是當(dāng)前時(shí)間對(duì)于第二個(gè) TIMESTAMP列,雖然通過(guò)語(yǔ)句show create table t1\G查出來(lái)的建表語(yǔ)句指定的默認(rèn)值是'0000-00-00 00:00:00'但是存到表里的卻是當(dāng)前時(shí)間最奇怪的是第三個(gè) TIMESTAMP列,盡管我們顯式指定默認(rèn)值為'2010-01-01 00:00:00',但是落表的時(shí)間仍然是當(dāng)前時(shí)間
這一切都是在參數(shù)explicit_defaults_for_timestamp被設(shè)置為OFF的時(shí)候發(fā)生的,但是遺憾的是OFF恰恰就是參數(shù)explicit_defaults_for_timestamp的默認(rèn)值。

explicit_defaults_for_timestamp的值改為ON,則事情會(huì)變得好很多mysql>?show?variables?like?'explicit_defaults_for_timestamp';
+---------------------------------+-------+
|?Variable_name???????????????????|?Value?|
+---------------------------------+-------+
|?explicit_defaults_for_timestamp?|?ON????|
+---------------------------------+-------+
mysql>?create?table?t2?
????->?(
????->?ts1?timestamp,
????->?ts2?timestamp,
????->?ts3?timestamp?default?'2010-01-01?00:00:00'
????->?);
Query?OK,?0?rows?affected?(0.02?sec)
mysql>?show?create?table?t2\G
***************************?1.?row?***************************
???????Table:?t2
Create?Table:?CREATE?TABLE?`t2`?(
??`ts1`?timestamp?NULL?DEFAULT?NULL,
??`ts2`?timestamp?NULL?DEFAULT?NULL,
??`ts3`?timestamp?NULL?DEFAULT?'2010-01-01?00:00:00'
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8
1?row?in?set?(0.01?sec)
mysql>?insert?into?t2?values?(null,null,null);
Query?OK,?1?row?affected?(0.01?sec)
mysql>?select?*?from?t2;
+------+------+------+
|?ts1??|?ts2??|?ts3??|
+------+------+------+
|?NULL?|?NULL?|?NULL?|
+------+------+------+
1?row?in?set?(0.00?sec)
這一次,建表語(yǔ)句中那些奇怪的默認(rèn)值都沒(méi)有了,清爽了好多,而且TIMESTAMP的的列也可以插入NULL了,如果我們顯式指定了NOT NULL,STRICT_TRANS_TABLES被指定的情況下直接報(bào)錯(cuò),如果STRICT_TRANS_TABLES沒(méi)有被指定,那么會(huì)向該列中插入0000-00-00 00:00:00并且產(chǎn)生一個(gè)warning
mysql>?create?table?t3?
????->?(
????->?ts1?timestamp,
????->?ts2?timestamp,
????->?ts3?timestamp?not?null
????->?);
Query?OK,?0?rows?affected?(0.01?sec)
mysql>?show?create?table?t3\G
***************************?1.?row?***************************
???????Table:?t3
Create?Table:?CREATE?TABLE?`t3`?(
??`ts1`?timestamp?NULL?DEFAULT?NULL,
??`ts2`?timestamp?NULL?DEFAULT?NULL,
??`ts3`?timestamp?NOT?NULL
)?ENGINE=InnoDB?DEFAULT?CHARSET=utf8
1?row?in?set?(0.01?sec)
mysql>?insert?into?t3?values?(null,null,null);
ERROR?1048?(23000):?Column?'ts3'?cannot?be?null
mysql>?insert?into?t3?(ts1,ts2)?values?(null,null);
Query?OK,?1?row?affected,?1?warning?(0.01?sec)
mysql>?show?warnings;
+---------+------+------------------------------------------+
|?Level???|?Code?|?Message??????????????????????????????????|
+---------+------+------------------------------------------+
|?Warning?|?1364?|?Field?'ts3'?doesn't?have?a?default?value?|
+---------+------+------------------------------------------+
mysql>?select?*?from?t3;
+------+------+---------------------+
|?ts1??|?ts2??|?ts3?????????????????|
+------+------+---------------------+
|?NULL?|?NULL?|?0000-00-00?00:00:00?|
+------+------+---------------------+
高并發(fā)環(huán)境下并不適合使用TIMESTAMP
這一點(diǎn)MySql的文檔中有明確的說(shuō)明:
Note
If set to
SYSTEM, every MySQL function call that requires a time zone calculation makes a system library call to determine the current system time zone. This call may be protected by a global mutex, resulting in contention.
雖然通過(guò)TIMESTAMP可以自動(dòng)轉(zhuǎn)換時(shí)區(qū),代價(jià)是當(dāng)MySQL參數(shù)time_zone=system時(shí)每次都會(huì)嘗試獲取一個(gè)全局鎖,這在高并發(fā)的環(huán)境下無(wú)疑是致命的,可能會(huì)導(dǎo)致線程上下文頻繁切換,CPU使用率暴漲,系統(tǒng)響應(yīng)變慢甚至假死。點(diǎn)擊這個(gè)Java面試庫(kù)小程序可以在線刷題。
時(shí)間范圍并不是強(qiáng)校驗(yàn)的
如果我們嘗試往MySql中插入超過(guò)TIMESTAMP可表示的時(shí)間范圍的值,MySql在非嚴(yán)格模式下并不會(huì)報(bào)錯(cuò),僅會(huì)產(chǎn)生一個(gè)warning
mysql> insert into t1 values ('2039-01-01 00:00:00',null,null);
Query OK, 1 row affected, 1 warning (0.00 sec)
mysql> show warnings;
+---------+------+----------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------+
| Warning | 1264 | Out of range value for column 'ts1' at row 1 |
+---------+------+----------------------------------------------+
1 row in set (0.00 sec)
mysql> select * from t1;
+---------------------+---------------------+---------------------+
| ts1 | ts2 | ts3 |
+---------------------+---------------------+---------------------+
| 2021-05-09 07:47:50 | 2021-05-09 07:47:50 | 2021-05-09 07:47:50 |
| 0000-00-00 00:00:00 | 2021-05-09 08:09:06 | 2021-05-09 08:09:06 |
+---------------------+---------------------+---------------------+
2 rows in set (0.00 sec)
總結(jié)
現(xiàn)在用TIMESTAMP比較少了,的確也應(yīng)該盡量避免使用TIMESTAMP,MySql在TIMESTAMP的設(shè)計(jì)上實(shí)在是蹩腳,如果你正在維護(hù)一個(gè)老的系統(tǒng),涉及到TIMESTAMP的改動(dòng)需要格外注意,盡量要在充分的測(cè)試后再上線。
最后,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Java/ MySQL 系列面試題和答案,非常齊全。
參考資料







關(guān)注Java技術(shù)棧看更多干貨


