mysql之TIMESTAMP(時間戳)用法詳解以及存在風險


參考:https://www.jb51.net/article/51794.htm

參考:https://zhuanlan.zhihu.com/p/380870673

時間戳是指格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數。
生產環境中部署着各種版本的MySQL,包括MySQL 5.5/5.6/5.7三個大版本和N個小版本,由於MySQL在向上兼容性較差,導致相同SQL在不同版本上表現各異,下面從幾個方面來詳細介紹時間戳數據類型。

時間戳數據存取

在MySQL上述三個大版本中,默認時間戳(Timestamp)類型的取值范圍為'1970-01-01 00:00:01' UTC 至'2038-01-19 03:14:07' UTC,數據精確到秒級別,該取值范圍包含約22億個數值,因此在MySQL內部使用4個字節INT類型來存放時間戳數據:

1、在存儲時間戳數據時,先將本地時區時間轉換為UTC時區時間,再將UTC時區時間轉換為INT格式的毫秒值(使用UNIX_TIMESTAMP函數),然后存放到數據庫中。
2、在讀取時間戳數據時,先將INT格式的毫秒值轉換為UTC時區時間(使用FROM_UNIXTIME函數),然后再轉換為本地時區時間,最后返回給客戶端。

在MySQL 5.6.4及之后版本,可以將時間戳類型數據最高精確微秒(百萬分之一秒),數據類型定義為timestamp(N),N取值范圍為0-6,默認為0,如需要精確到毫秒則設置為Timestamp(3),如需要精確到微秒則設置為timestamp(6),數據精度提高的代價是其內部存儲空間的變大,但仍未改變時間戳類型的最小和最大取值范圍。

時間戳字段定義

時間戳字段定義主要影響兩類操作:

  • 插入記錄時,時間戳字段包含DEFAULT CURRENT_TIMESTAMP,如插入記錄時未指定具體時間數據則將該時間戳字段值設置為當前時間
  • 更新記錄時,時間戳字段包含ON UPDATE CURRENT_TIMESTAMP,如更新記錄時未指定具體時間數據則將該時間戳字段值設置為當前時間

PS1:CURRENT_TIMESTAMP表示使用CURRENT_TIMESTAMP()函數來獲取當前時間,類似於NOW()函數

根據上面兩類操作,時間戳列可以有四張組合定義,其含義分別為:

  • 當字段定義為timestamp,表示該字段在插入和更新時都不會自動設置為當前時間。
  • 當字段定義為timestamp DEFAULT CURRENT_TIMESTAMP,表示該字段僅在插入且未指定值時被賦予當前時間,再更新時且未指定值時不做修改。
  • 當字段定義為timestamp ON UPDATE CURRENT_TIMESTAMP,表示該字段在插入且未指定值時被賦值為"0000-00-00 00:00:00",在更新且未指定值時更新為當前時間。
  • 當字段定義為timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,表示該字段在插入或更新時未指定值,則被賦值為當前時間。

PS1:在MySQL中執行的建表語句和最終表創建語句會存在差異,建議使用SHOW CREATE TABLE TB_XXX獲取已創建表的建表語句。

時間戳字段在MySQL各版本的使用差異

在MySQL 5.5及之前版本中,僅能對一個時間戳字段定義DEFUALT CURRENT_TIMESTAMP或ON UPDATE CURRENT_TIMESTAMP,但在MySQL 5.6和MySQL 5.7版本中取消了該限制;

在MySQL 5.6版本中參數explicit_defaults_for_timestamp默認值為1,在MySQL 5.7版本中參數explicit_defaults_for_timestamp默認值為0;

在MySQL 5.5和MySQL 5.7版本中timestamp類型默認為NOT NULL,在在MySQL 5.6版本中timestamp類型默認為NULL;

當建表語句中定於c1 timestamp 時,

在MySQL 5.5中等價於c1 timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

在MySQL 5.6中等價於c1 timestamp NULL DEFAULT NULL;

在MySQL 5.7中等價於c1 timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

當建表語句中c1 timestamp default 0時,

在MySQL 5.5中等價於c1 timestamp NOT NULL DEFAULT ‘0000-00-00 00:00:00';
在MySQL 5.6中等價於c1 timestamp NULL DEFAULT ‘0000-00-00 00:00:00';
在MySQL 5.7中等價於c1 timestamp NOT NULL DEFAULT ‘0000-00-00 00:00:00';
PS1: MySQL 5.6版本和MySQL 5.7版本中主要差異受參數explicit_defaults_for_timestamp的默認值影響。

PS2:當時間戳列的默認值為'0000-00-00 00:00:00'時,使用“不在時間戳取值范圍內”的該默認值並不會產生警告。

時間戳類型引發的異常

  1. 當MySQL參數time_zone=system時,查詢timestamp字段會調用系統時區做時區轉換,而由於系統時區存在全局鎖問題,在多並發大數據量訪問時會導致線程上下文頻繁切換,CPU使用率暴漲,系統響應變慢甚至假死。
  2. 如果表中包含TIMESTAMP的列,那么其建表語句有可能被系統篡改,取決於MySql的版本和參數設置。
  3. 如果存入超過范圍的時間,在非嚴格狀態下,MySql不會報錯,反而會插入'0000-00-00 00:00:00'

問題一:高並發下的問題

這一點MySql的文檔中有明確的說明:

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.

雖然通過TIMESTAMP可以自動轉換時區,代價是當MySQL參數time_zone=system時每次都會嘗試獲取一個全局鎖,這在高並發的環境下無疑是致命的,可能會導致線程上下文頻繁切換,CPU使用率暴漲,系統響應變慢甚至假死。

問題二:如果表中包含TIMESTAMP的列,那么其建表語句有可能被系統篡改

MySql 5.6.6版本引入了explicit_defaults_for_timestamp這個參數,隨即被標記為廢棄,這個參數主要影響表中類型為TIMESTAMP的那些列在新建表時的表現

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)

雖然我們輸入的建表語句很簡單,但是MySql卻對於我們輸入的建表語句做了諸多的篡改:

  • 對於表中的第一個TIMESTAMP列,系統自動加了NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,這些操作對於新建表的開發者完全是不感知的。
  • 對於表中的第二個TIMESTAMP列,系統自動加了一個默認值0000-00-00 00:00:00,這個操作同樣對於新建表的開發者完全不感知。

在系統對我們的建表語句做了自動修改之后,對表的插入操作可能就不會如開發者預期的那樣:

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的表現非常的鬼畜

  • 對於第一個TIMESTAMP列,建表語句中指定可以為null,但是插入null的時候存到表里的卻是當前時間
  • 對於第二個TIMESTAMP列,雖然通過語句show create table t1\G查出來的建表語句指定的默認值是'0000-00-00 00:00:00'但是存到表里的卻是當前時間
  • 最奇怪的是第三個TIMESTAMP列,盡管我們顯式指定默認值為'2010-01-01 00:00:00',但是落表的時間仍然是當前時間

這一切都是在參數explicit_defaults_for_timestamp被設置為OFF的時候發生的,但是遺憾的是OFF恰恰就是參數explicit_defaults_for_timestamp的默認值。

如果我們將explicit_defaults_for_timestamp的值改為ON,則事情會變得好很多

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)

這一次,建表語句中那些奇怪的默認值都沒有了,清爽了好多,而且TIMESTAMP的的列也可以插入NULL了,如果我們顯式指定了NOT NULLSTRICT_TRANS_TABLES被指定的情況下直接報錯,如果STRICT_TRANS_TABLES沒有被指定,那么會向該列中插入0000-00-00 00:00:00並且產生一個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 |
+------+------+---------------------+

問題三:時間范圍並不是強校驗的

如果我們嘗試往MySql中插入超過TIMESTAMP可表示的時間范圍的值,MySql在非嚴格模式下並不會報錯,僅會產生一個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)

時間戳類型和時間類型選擇

在部分"數據庫指導"文檔中,會推薦使用timestamp類型代替datetime字段,其理由是timestamp類型使用4字節,而datetime字段使用8字節,但隨着磁盤性能提升和內存成本降低,在實際生產環境中,使用timestamp類型並不會帶來太多性能提升,反而可能因timestamp類型的定義和取值范圍限制和影響業務使用。

在MySQL 5.6.4及之后版本,可以將時間戳類型(timestamp)數據最高精確微秒,也同樣可以將時間類型(datetime)數據最高精確微秒,時間類型(datetime)同樣可以獲得timestamp類型相同的效果,如將字段定義為 dt1 DATETIME(3) NOT NULL DEFAULT NOW(3) ON UPDATE NOW(3); 時間類型(datetime)的存取范圍'1000-01-01 00:00:00.000000' 至 ‘9999-12-31 23:59:59.999999',能更好地存放各時間段的數據。

時間戳類型使用建議

  • 在只關心數據最后更新時間的情況下,建議將時間戳列定義為TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;
  • 在關心創建時間和更新時間的情況下,建議將更新時間設置為時間戳字段,將創建時間定義為DAETIME 或 TIMESTAMP DEFAULT ‘0000-00-00 00:00:00',並在插入記錄時顯式指定創建時間;
  • 建議在表中只定義單個時間戳列,並顯式定義DEFAULT 和 ON UPDATE屬性;
  • 雖然在MySQL中可以對時間戳字段賦值或更新,但建議僅在必要的情況下對時間戳列進行顯式插入和更新;
  • 建議將time_zone參數設置為system外的值,如中國地區服務器設置為'+8:00';
  • 建議將MySQL線下測試版本和線上生產版本保持一致。

Timestamp和datetime的異同

相同點:

1.可自動更新和初始化,默認顯示格式相同YYYY-MM-dd HH:mm:ss
不同點:
2. timestamp的時間范圍是:‘1970-01-01 00:00:01' UTC to ‘2038-01-19 03:14:07' UTC ,自動時區轉化,實際存儲毫秒數,4字節存儲
3. datetime的時間范圍:‘1000-01-01 00:00:00' to ‘9999-12-31 23:59:59' ,不支持時區,8字節存儲

總結

現在用TIMESTAMP比較少了,的確也應該盡量避免使用TIMESTAMPMySqlTIMESTAMP的設計上實在是蹩腳,如果你正在維護一個老的系統,涉及到TIMESTAMP的改動需要格外注意,盡量要在充分的測試后再上線。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM