PHP和mySQL:2038年问题:它是什么?如何解决?

146

我本想使用TIMESTAMP来存储日期+时间,但我了解到它有一个2038年的限制。为了使新手用户也能轻松理解,而不是批量提问,我更倾向于把问题分成小部分。所以我的问题如下:

  1. 什么是2038年问题?
  2. 为什么会发生这个问题?当它出现时会发生什么?
  3. 我们该如何解决这个问题?
  4. 是否存在其他替代方法,不会出现类似的问题?
  5. 对于已经使用TIMESTAMP的现有应用程序,当真正发生问题时,我们该怎么做以避免所谓的问题?

34
Gordon,我制作了一个预测应用程序。我会保存每个月拥有的资金数量,并估算何时能成为百万富翁。在我的情况下,28年并不算太长时间,我相信现在也有其他人遇到这个问题(我使用64位数字来表示时间戳来解决此问题)。 - Emil Vikström
2
通过使用64位整数,您已经找到了一个具体问题的简单解决方案。这并不适用于(或不需要)每个人,但对于您的用例有效。关键是,如果OP没有像预测这样的具体问题,那么这可能是一个有趣的话题,但他不应该担心解决这个一般性的问题,因为这不是PHP(请注意标签)的问题。这只是我的个人意见。 - Gordon
2
到2038年,解析字符串“YYYY-MM-DD HH:MM:SS:mmmm…”将成为您可以梦想的最便宜的操作。到2038年,32位将过时。我怀疑我们所知道的Unix时间戳是否仍然存在,如果确实存在,我们的256位系统将处理远远超过4096位系统在快乐餐中分发的年龄的日期。 - Super Cat
18
戈登,现在已经过去了9年。时间戳仍然被使用。我们仍在使用28年前的技术,它叫做万维网。 - sfscs
2
我会进行评论,因为我想看看这段对话的发展。 - Shod
显示剩余3条评论
7个回答

194

我已将此标记为社区维基,因此随意编辑。

什么是2038年问题?

"2038年问题(也称为Unix千年虫,类比于Y2K问题的Y2K38)可能会导致某些计算机软件在2038年之前或之后失败。该问题影响所有将系统时间存储为带符号32位整数并将此数字解释为自1970年1月1日UTC 00:00:00以来过去的秒数的软件和系统。"


为什么会发生这种情况,它发生时会发生什么?

超过2038年1月19日星期二UTC 03:14:07 的时间将“循环”并被内部存储为负数,这些系统将将其解释为1901年12月13日而不是2038年的时间。这是由于自UNIX纪元(1970年1月1日00:00:00 GMT)以来的秒数将超过计算机32位符号整数的最大值。


我们该如何解决它?

  • 使用长数据类型(64位足够)
  • 对于MySQL(或MariaDB),如果您不需要时间信息,请考虑使用 DATE 列类型。如果需要更高的精度,请使用 DATETIME 而不是 TIMESTAMP 。请注意, DATETIME 列不保存有关时区的信息,因此您的应用程序必须知道使用了哪个时区。
  • 维基百科上描述的其他可能解决方案
  • 将Mysql升级到8.0.28或更高版本

是否有任何可能的替代方案,不会出现类似的问题?

在数据库中尽可能使用大型类型来存储日期:64位足够 - 在GNU C和POSIX / SuS中是long long类型,或在PHP或BCmath扩展中使用sprintf('%u'…)。


即使我们还没有到2038年,也有哪些潜在的破坏性用例?

因此,MySQL DATETIME 的范围为1000-9999,但TIMESTAMP的范围只有1970-2038。如果您的系统存储出生日期、未来的日期(例如30年抵押贷款)或类似日期,那么您已经会遇到此错误。同样,如果这将成为一个问题,请不要使用TIMESTAMP。


当真正发生这个所谓的问题时,我们能对使用TIMESTAMP的现有应用程序做些什么来避免它?

尽管Web几乎还不是传统平台,但很少有PHP应用程序会在2038年之后存在。

以下是将数据库表列从TIMESTAMP转换为DATETIME的过程。该过程始于创建一个临时列:

# rename the old TIMESTAMP field
ALTER TABLE `myTable` CHANGE `myTimestamp` `temp_myTimestamp` int(11) NOT NULL;

# create a new DATETIME column of the same name as your old column
ALTER TABLE `myTable` ADD `myTimestamp` DATETIME NOT NULL;

# update all rows by populating your new DATETIME field
UPDATE `myTable` SET `myTimestamp` = FROM_UNIXTIME(temp_myTimestamp);

# remove the temporary column
ALTER TABLE `myTable` DROP `temp_myTimestamp`

资源


2
太棒了。对我来说,你的答案是最完整的。非常感谢。 - Devner
10
在MySQL中,如果将来的版本更改了TIMESTAMP的基础存储数据类型为64位,则无需将代码更改为DATETIME,对吗?我只是认为不应该阻止任何人使用TIMESTAMP,因为该数据类型确实有其用途。 - pixelfreak
6
MySQL(和MariaDB)在64位系统上不使用64位整数来存储时间戳是绝对荒谬的。使用DATETIME不是一个足够的解决方案,因为我们无法确定时区。这个问题早在2005年就已经报告过了,但仍然没有修复措施。 - Mike
3
我对“2038年时PHP应用程序将很少存在”这一说法持有异议,因为它听起来像是毫无根据的观点。更不用说这个回答可能在未来5年、10年甚至15年仍会存在于网络上了。 - DAG
2
TIMESTAMP类型的行为也不受此更改的影响;其允许的最大值仍为“2038-01-19”。 - Brad
显示剩余11条评论

18
使用UNIX时间戳存储日期时,实际上使用的是32位整数,它记录了自1970年01月01日以来的秒数;请参见Unix Time
那个32位数字将在2038年溢出。这就是2038年问题。
为解决该问题,您不应使用32位UNIX时间戳来存储日期--这意味着,在使用MySQL时,您不应使用TIMESTAMP,而应使用DATETIME(请参见10.3.1. The DATETIME, DATE, and TIMESTAMP Types):

当您需要包含日期和时间信息的值时,使用DATETIME类型。支持的范围是'1000-01-01 00:00:00''9999-12-31 23:59:59'

TIMESTAMP数据类型的范围为'1970-01-01 00:00:01' UTC到'2038-01-19 03:14:07' UTC。


避免/修复这个问题,你可以对含有不在1970年到2038年之间的日期的列使用DATETIME而非TIMESTAMP

需要注意的是:根据统计学来看,很有可能在2038年之前你的应用程序已经被重写了很多次^^因此,如果你不必处理未来的日期,也许在当前版本的应用程序中就不必担心这个问题了...


1
+1 为这份信息点赞。这里的尝试是从一开始就适应最佳实践,以便后来不必担心问题。因此,尽管现在2038年可能听起来不是问题,但我只想遵循最佳实践,并从一开始就为我创建的每个应用程序遵循它。希望这有意义。 - Devner
是的,它很有道理,我明白你的观点。但是,“最佳实践”也可以意味着“满足需求的最佳方案”-- 如果你知道时间戳足够了,就没有必要使用日期时间(例如,后者需要更多的存储空间;如果你有数百万条记录,这可能很重要);; 我有一些应用程序,在其中我对某些列使用时间戳(例如包含当前日期的列),对于另一些列则使用日期时间(例如包含过去/未来日期的列)。 - Pascal MARTIN
我看到你的程序也很有道理,尤其是在涉及存储空间时表现良好。如果可以的话,我想问一下你在应用程序中通常使用什么结构和长度来处理TIMESTAMP列?谢谢。 - Devner
当我想要使用 TIMESTAMP 时,是因为我的“日期/时间”数据适用于1970年至2038年之间,因此我使用 MySQL TIMESTAMP 数据类型。 - Pascal MARTIN
谢谢提供的信息。根据您的回复,我理解只需将列声明为TIMESTAMP即可,无需为其提供任何长度(不像int或var,我们需要提供长度)。我是正确的吗? - Devner
@Devner:你是对的:在MySQL中,TIMESTAMP始终使用4个字节存储--即UNIX时间戳的32位。 - Pascal MARTIN

7

在谷歌上快速搜索即可解决:2038年问题

  1. 2038年问题(也称Unix千年虫,类比于Y2K问题的Y2K38)可能会导致一些计算机软件在2038年之前或之后失效。
  2. 该问题影响所有将系统时间存储为带符号32位整数,并将此数字解释为从1970年1月1日00:00:00 UTC起的秒数的软件和系统。以这种方式表示的最新时间是UTC时间2038年1月19日星期二03:14:07。超过此时刻的时间将“绕回”,并在内部存储为负数,这些系统将将其解释为1901年而不是2038年的日期。
  3. 对于现有CPU / OS组合、现有文件系统或现有二进制数据格式,这个问题没有简单的解决方案。

没有回答关于如何处理 PHP/MySQL 问题的具体问题。 - Jeremy L.

2

http://en.wikipedia.org/wiki/Year_2038_problem详细描述了问题。

总结如下:

1) + 2) 问题在于许多系统将日期信息存储为32位有符号整数,表示自1970年1月1日以来的秒数。最后可存储的日期是在2038年1月19日星期二UTC时间03:14:07。当这种情况发生时,整数将“回绕”,并存储为负数,被解释为1901年的日期。具体会发生什么取决于各个系统,但可以肯定的是,对任何一个系统来说都不会是好事!

对于只记录过去日期的系统,您可能暂时不需要担心!主要问题在于需要处理未来日期的系统。如果您的系统需要处理28年后的日期,则现在应该开始担心了!

3) 使用可用的替代日期格式之一,或使用64位系统并使用64位整数。或对于数据库,使用替代时间戳格式(例如,对于MySQL使用DATETIME)。

4) 参见第3条!

5) 参见第4条!!!;)


你的第四点和第五点让我想起了“按引用传递”。这让我感到很开心。谢谢你,给你一个赞。 - Devner

1
兄弟们,如果你需要使用PHP来显示时间戳,这是最好的PHP解决方案,而不需要改变UNIX_TIMESTAMP格式。
使用custom_date()函数。在其中,使用DateTime。这是DateTime的解决方案
只要数据库中的时间戳为UNSIGNED BIGINT(8)格式,只要您有PHP 5.2.0 ++。

0

最近我在思考这些问题,并想分享我为新项目找到的解决方案。

Bigint

在阅读了各种类似问题的回答后,我发现将 Unix 时间戳存储在 Bigint 列中是一个更好的解决方案。

Bigint 的范围将覆盖您从时间开始之前一直到年份 292277026596,可以说是永远。

此外:

  • 它使用与 DATETIME 相同的 8 字节存储。
  • 它不受时区影响。
  • 您仍然可以通过 DEFAULT (unix_timestamp()) 使用自动生成的时间戳。
  • 即使以毫秒为单位存储时间,其范围也非常大,您的服务器在包装发生之前就会变成灰尘。

DECIMAL

这是我找到的解决方案,因为它可以获得更多的控制。您可以存储一个过度的日期范围,如bigint,或将其减少到实际可行的范围并使用更少的存储空间。
在我的情况下,我还想将秒的分数存储为实际的分数,并且我仍然希望在插入时生成时间戳。
以下是我创建表模式中的列定义:
`created` decimal(18,2) NOT NULL DEFAULT (unix_timestamp(now(2))) COMMENT 'unix timestamp'

使用decimal(18,2)提供了绝对过度的时间范围,与bigint/datetime相同的存储空间,同时显示到2位小数。

存储基于数字的数量,9个数字=4字节有符号或无符号都无所谓。

您可以将范围限制在更实际的范围内,并且比datetime少得多,或者将精度增加到纳秒。您决定什么是重要的。

另一个优点是,如果在遥远的未来您达到了范围的极限,Mysql会告诉您。 不会发生环绕问题,而是会出现插入错误,可以轻松地再次更改表以添加另一个数字。

这对我来说非常合理,我强烈建议使用这种方法开始新的数据库。


-2

由于我不想升级任何东西,因此我让我的后端(MSSQL)来完成这项工作,而不是PHP!

$qry = "select DATEADD(month, 1, :date) next_date ";
$rs_tmp = $pdo->prepare($qry);
$rs_tmp->bindValue(":date", '2038/01/15');
$rs_tmp->execute();
$row_tmp = $rs_tmp->fetch(PDO::FETCH_ASSOC);

echo $row_tmp['next_date'];

可能不是最有效的方法,但它可行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接